Skip to content

Commit

Permalink
Merge pull request #604 from CDCgov/task-600-cached_secrets
Browse files Browse the repository at this point in the history
600: Cached Our Private Key Only After Successful Auth with RS
  • Loading branch information
halprin authored Oct 25, 2023
2 parents 50e4510 + 5f4c598 commit 6195a7d
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ public class ReportStreamOrderSender implements OrderSender {
.map(urlPrefix -> urlPrefix.replace("https://", "").replace("http://", ""))
.orElse("");

private static final String OUR_PRIVATE_KEY_ID =
"trusted-intermediary-private-key-" + ApplicationContext.getEnvironment();

private static final String CLIENT_NAME = "flexion.etor-service-sender";
private static final Map<String, String> RS_AUTH_API_HEADERS =
Map.of("Content-Type", "application/x-www-form-urlencoded");

private String rsTokenCache;

Expand Down Expand Up @@ -131,42 +136,53 @@ protected String sendRequestBody(@Nonnull String json, @Nonnull String bearerTok
protected String requestToken() throws UnableToSendOrderException {
logger.logInfo("Requesting token from ReportStream");

String senderToken = null;
String token = "";
String body;
Map<String, String> headers = Map.of("Content-Type", "application/x-www-form-urlencoded");
String ourPrivateKey;
String token;

try {
senderToken =
ourPrivateKey = retrievePrivateKey();
String senderToken =
jwt.generateToken(
CLIENT_NAME,
CLIENT_NAME,
CLIENT_NAME,
RS_DOMAIN_NAME,
300,
retrievePrivateKey());
body = composeRequestBody(senderToken);
String rsResponse = client.post(RS_AUTH_API_URL, headers, body);
ourPrivateKey);
String body = composeRequestBody(senderToken);
String rsResponse = client.post(RS_AUTH_API_URL, RS_AUTH_API_HEADERS, body);
token = extractToken(rsResponse);
} catch (Exception e) {
throw new UnableToSendOrderException(
"Error getting the API token from ReportStream", e);
}

// only cache our private key if we successfully authenticate to RS
cacheOurPrivateKeyIfNotCachedAlready(ourPrivateKey);

return token;
}

protected String retrievePrivateKey() throws SecretRetrievalException {
var senderPrivateKey =
"trusted-intermediary-private-key-" + ApplicationContext.getEnvironment();
String key = this.keyCache.get(senderPrivateKey);
String key = keyCache.get(OUR_PRIVATE_KEY_ID);
if (key != null) {
return key;
}

key = secrets.getKey(senderPrivateKey);
this.keyCache.put(senderPrivateKey, key);
key = secrets.getKey(OUR_PRIVATE_KEY_ID);

return key;
}

void cacheOurPrivateKeyIfNotCachedAlready(String privateKey) {
String key = keyCache.get(OUR_PRIVATE_KEY_ID);
if (key != null) {
return;
}

keyCache.put(OUR_PRIVATE_KEY_ID, privateKey);
}

protected String extractToken(String responseBody) throws FormatterProcessingException {
var value =
formatter.convertJsonToObject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,100 @@ class ReportStreamOrderSenderTest extends Specification {
TestApplicationContext.register(Secrets, mockSecrets)
TestApplicationContext.register(Cache, mockCache)
TestApplicationContext.injectRegisteredImplementations()

when:
mockSecrets.getKey(_ as String) >> "Fake Azure Key"
def actual = ReportStreamOrderSender.getInstance().requestToken()

then:
1 * mockAuthEngine.generateToken(_ as String, _ as String, _ as String, _ as String, 300, _ as String) >> "sender fake token"
1 * mockClient.post(_ as String, _ as Map<String, String>, _ as String) >> """{"access_token":"${expected}", "token_type":"bearer"}"""
actual == expected
}

def "requestToken saves our private key only after successful call to RS"() {
given:
def mockSecrets = Mock(Secrets)
def mockCache = Mock(Cache)
def mockFormatter = Mock(Formatter)

def fakeOurPrivateKey = "DogCow" // pragma: allowlist secret
mockSecrets.getKey(_ as String) >> fakeOurPrivateKey
mockFormatter.convertJsonToObject(_ , _) >> [access_token: "Moof!"]

TestApplicationContext.register(AuthEngine, Mock(AuthEngine))
TestApplicationContext.register(HttpClient, Mock(HttpClient))
TestApplicationContext.register(Formatter, mockFormatter)
TestApplicationContext.register(Secrets, mockSecrets)
TestApplicationContext.register(Cache, mockCache)

TestApplicationContext.injectRegisteredImplementations()

when:
ReportStreamOrderSender.getInstance().requestToken()

then:
1 * mockCache.put(_ as String, fakeOurPrivateKey)
}

def "requestToken doesn't cache our private key if RS auth call fails"() {
given:
def mockClient = Mock(HttpClient)
def mockCache = Mock(Cache)
def mockFormatter = Mock(Formatter)

mockClient.post(_, _, _) >> { throw new HttpClientException("Fake failure", new NullPointerException()) }

mockFormatter.convertJsonToObject(_ , _) >> [access_token: "Moof!"]

TestApplicationContext.register(AuthEngine, Mock(AuthEngine))
TestApplicationContext.register(HttpClient, mockClient)
TestApplicationContext.register(Formatter, mockFormatter)
TestApplicationContext.register(Secrets, Mock(Secrets))
TestApplicationContext.register(Cache, mockCache)

TestApplicationContext.injectRegisteredImplementations()

when:
ReportStreamOrderSender.getInstance().requestToken()

then:
thrown(UnableToSendOrderException)
0 * mockCache.put(_ , _)
}

def "cacheOurPrivateKeyIfNotCachedAlready doesn't cache when the key is already is cached"() {
given:
def mockCache = Mock(Cache)
mockCache.get(_ as String) >> "DogCow private key"

TestApplicationContext.register(Cache, mockCache)

TestApplicationContext.injectRegisteredImplementations()

when:
ReportStreamOrderSender.getInstance().cacheOurPrivateKeyIfNotCachedAlready("Moof!")

then:
0 * mockCache.put(_, _)
}

def "cacheOurPrivateKeyIfNotCachedAlready caches when the key isn't cached"() {
given:
def mockCache = Mock(Cache)
mockCache.get(_ as String) >> null

TestApplicationContext.register(Cache, mockCache)

TestApplicationContext.injectRegisteredImplementations()

when:
ReportStreamOrderSender.getInstance().cacheOurPrivateKeyIfNotCachedAlready("Moof!")

then:
1 * mockCache.put(_, _)
}

def "extractToken works"() {
given:
TestApplicationContext.register(Formatter, Jackson.getInstance())
Expand Down Expand Up @@ -193,19 +278,16 @@ class ReportStreamOrderSenderTest extends Specification {
given:
def mockSecret = Mock(Secrets)
def expected = "New Fake Azure Key"
def keyCache = KeyCache.getInstance()
def key = "trusted-intermediary-private-key-local"
mockSecret.getKey(_ as String) >> expected
TestApplicationContext.register(Secrets, mockSecret)
TestApplicationContext.register(Cache, keyCache)
TestApplicationContext.register(Cache, KeyCache.getInstance())
TestApplicationContext.injectRegisteredImplementations()
def rsOrderSender = ReportStreamOrderSender.getInstance()
when:
def actual = rsOrderSender.retrievePrivateKey()

then:
actual == expected
keyCache.get(key) == expected
}

def "retrievePrivateKey works when cache is not empty" () {
Expand Down

0 comments on commit 6195a7d

Please sign in to comment.