Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

600: Cached Our Private Key Only After Successful Auth with RS #604

Merged
merged 9 commits into from
Oct 25, 2023
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();
jcrichlake marked this conversation as resolved.
Show resolved Hide resolved
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);
jcrichlake marked this conversation as resolved.
Show resolved Hide resolved
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
cachePrivateKeyIfNotCachedAlready(ourPrivateKey);
jcrichlake marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@jorg3lopez jorg3lopez Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work here. I wonder since cachePrivateKeyIfNotCachedAlready() deals with only our private key, what are your thoughts on renaming it cacheOurPrivateKeyIfNotCachedAlready()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I'll do that!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For record keeping. We still need to have a system in place that will change our private key in the cache, when it expires or when it is not valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! Thanks for calling that out and adding that item to our engineering task list channel.

}

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

return key;
}

protected void cachePrivateKeyIfNotCachedAlready(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 "cachePrivateKeyIfNotCachedAlready 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().cachePrivateKeyIfNotCachedAlready("Moof!")

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

def "cachePrivateKeyIfNotCachedAlready 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().cachePrivateKeyIfNotCachedAlready("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
Loading