Skip to content

Commit

Permalink
Use SSM directly instead of storing sensitive info in env vars
Browse files Browse the repository at this point in the history
  • Loading branch information
casewalker committed Nov 27, 2023
1 parent 69b6c38 commit c500277
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 44 deletions.
17 changes: 9 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ dependencies {
// From the Serverless generated code
'com.amazonaws:aws-lambda-java-core:1.2.3',
'com.amazonaws:aws-lambda-java-log4j:1.0.1',
// For getting the Cognito UserPool Group Description and reading YAML
'software.amazon.awssdk:cognitoidentityprovider:2.20.161',
// For getting the Cognito UserPool Group Description, reading YAML, and accessing SSM
'software.amazon.awssdk:cognitoidentityprovider:2.21.29',
'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2',
'software.amazon.awssdk:ssm:2.21.29',
// For SAML generation, from Keycloak
'org.keycloak:keycloak-saml-core:22.0.4',
'org.keycloak:keycloak-adapter-core:22.0.4',
Expand All @@ -33,12 +34,12 @@ dependencies {
test {
environment([
// Provide env vars for KeyConstants + testing odds and ends
"KEY_PRIVATE_EXPONENT": "123",
"KEY_PRIME_P": "123",
"KEY_PRIME_Q": "123",
"KEY_PRIME_EXPONENT_P": "123",
"KEY_PRIME_EXPONENT_Q": "123",
"KEY_CRT_COEFFICIENT": "123",
"KEY_PRIVATE_EXPONENT_NAME": "custom-aws-idp-private-key-private-exponent",
"KEY_PRIME_P_NAME": "custom-aws-idp-private-key-prime-p",
"KEY_PRIME_Q_NAME": "custom-aws-idp-private-key-prime-q",
"KEY_PRIME_EXPONENT_P_NAME": "custom-aws-idp-private-key-prime-exponent-p",
"KEY_PRIME_EXPONENT_Q_NAME": "custom-aws-idp-private-key-prime-exponent-q",
"KEY_CRT_COEFFICIENT_NAME": "custom-aws-idp-private-key-crt-coefficient",
"COGNITO_REGION": "us-east-1",
"COGNITO_USER_POOL": "myPoolOfCoolUsers",
"DEFAULT_SESSION_DURATION": 900,
Expand Down
21 changes: 15 additions & 6 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ provider:
COGNITO_USER_POOL: "us-east-1_AZyvZQdFN"
PATH_PARAMETER_GROUP_NAME: "groupName"
# Get all secret key details from SSM
KEY_PRIVATE_EXPONENT: ${ssm:custom-aws-idp-private-key-private-exponent}
KEY_PRIME_P: ${ssm:custom-aws-idp-private-key-prime-p}
KEY_PRIME_Q: ${ssm:custom-aws-idp-private-key-prime-q}
KEY_PRIME_EXPONENT_P: ${ssm:custom-aws-idp-private-key-prime-exponent-p}
KEY_PRIME_EXPONENT_Q: ${ssm:custom-aws-idp-private-key-prime-exponent-q}
KEY_CRT_COEFFICIENT: ${ssm:custom-aws-idp-private-key-crt-coefficient}
KEY_PRIVATE_EXPONENT_NAME: "custom-aws-idp-private-key-private-exponent"
KEY_PRIME_P_NAME: "custom-aws-idp-private-key-prime-p"
KEY_PRIME_Q_NAME: "custom-aws-idp-private-key-prime-q"
KEY_PRIME_EXPONENT_P_NAME: "custom-aws-idp-private-key-prime-exponent-p"
KEY_PRIME_EXPONENT_Q_NAME: "custom-aws-idp-private-key-prime-exponent-q"
KEY_CRT_COEFFICIENT_NAME: "custom-aws-idp-private-key-crt-coefficient"
# Set a default session duration of 1 hour
DEFAULT_SESSION_DURATION: 3600
httpApi:
Expand All @@ -31,6 +31,15 @@ provider:
- Effect: Allow
Action: [ cognito-idp:GetGroup ]
Resource: "arn:aws:cognito-idp:us-east-1:274460373520:userpool/${self:provider.environment.COGNITO_USER_POOL}"
- Effect: Allow
Action: [ ssm:GetParameter, ssm:GetParameters ]
Resource:
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_PRIVATE_EXPONENT_NAME}"
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_PRIME_P_NAME}"
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_PRIME_Q_NAME}"
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_PRIME_EXPONENT_P_NAME}"
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_PRIME_EXPONENT_Q_NAME}"
- "arn:aws:ssm:us-east-1:274460373520:parameter/${self:provider.environment.KEY_CRT_COEFFICIENT_NAME}"

package:
artifact: build/distributions/customIdp.zip
Expand Down
35 changes: 20 additions & 15 deletions src/main/java/gov/nj/innovation/customAwsIdp/keys/KeyConstants.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package gov.nj.innovation.customAwsIdp.keys;

import gov.nj.innovation.customAwsIdp.util.SsmClientWrapper;
import org.bouncycastle.asn1.x500.X500Name;
import software.amazon.awssdk.services.ssm.SsmClient;

import java.math.BigInteger;
import java.util.Date;

/**
* Store the constants needed for generating keys; some which are public, and some which are secrets populated from
* environment variables.
* environment variables and SSM.
*
* @author Case Walker ([email protected])
*/
Expand Down Expand Up @@ -38,27 +40,30 @@ public record KeyConstants(
"99768495627357285435626145610679202888676240909221166688413303345247117760811"
);
private static final BigInteger KEY_PUBLIC_EXPONENT = new BigInteger("65537");
private static final String KEY_PRIVATE_EXPONENT_STR = System.getenv("KEY_PRIVATE_EXPONENT");
private static final String KEY_PRIME_P_STR = System.getenv("KEY_PRIME_P");
private static final String KEY_PRIME_Q_STR = System.getenv("KEY_PRIME_Q");
private static final String KEY_PRIME_EXPONENT_P_STR = System.getenv("KEY_PRIME_EXPONENT_P");
private static final String KEY_PRIME_EXPONENT_Q_STR = System.getenv("KEY_PRIME_EXPONENT_Q");
private static final String KEY_CRT_COEFFICIENT_STR = System.getenv("KEY_CRT_COEFFICIENT");
private static final X500Name CERT_SUBJECT = new X500Name("CN=AwsConnectStandaloneIdP");
private static final BigInteger CERT_SERIAL = new BigInteger("1696019667843");
private static final Date CERT_NOT_BEFORE = new Date(1696019567000L);
private static final Date CERT_NOT_AFTER = new Date(2011638867000L);
private static final String JCA_SIGNER_SIGNATURE_ALG = "SHA256WithRSA";

public KeyConstants() {
this(KEY_MODULUS,
// Names defined in the environment
private static final String KEY_PRIVATE_EXPONENT_NAME = "KEY_PRIVATE_EXPONENT_NAME";
private static final String KEY_PRIME_P_NAME = "KEY_PRIME_P_NAME";
private static final String KEY_PRIME_Q_NAME = "KEY_PRIME_Q_NAME";
private static final String KEY_PRIME_EXPONENT_P_NAME = "KEY_PRIME_EXPONENT_P_NAME";
private static final String KEY_PRIME_EXPONENT_Q_NAME = "KEY_PRIME_EXPONENT_Q_NAME";
private static final String KEY_CRT_COEFFICIENT_NAME = "KEY_CRT_COEFFICIENT_NAME";

public KeyConstants(final SsmClient ssmClient) {
this(
KEY_MODULUS,
KEY_PUBLIC_EXPONENT,
new BigInteger(KEY_PRIVATE_EXPONENT_STR),
new BigInteger(KEY_PRIME_P_STR),
new BigInteger(KEY_PRIME_Q_STR),
new BigInteger(KEY_PRIME_EXPONENT_P_STR),
new BigInteger(KEY_PRIME_EXPONENT_Q_STR),
new BigInteger(KEY_CRT_COEFFICIENT_STR),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_PRIVATE_EXPONENT_NAME))),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_PRIME_P_NAME))),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_PRIME_Q_NAME))),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_PRIME_EXPONENT_P_NAME))),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_PRIME_EXPONENT_Q_NAME))),
new BigInteger(SsmClientWrapper.getParameterByName(ssmClient, System.getenv(KEY_CRT_COEFFICIENT_NAME))),
CERT_SUBJECT,
CERT_SERIAL,
CERT_NOT_BEFORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Map;
import java.util.regex.Pattern;

import com.google.common.annotations.VisibleForTesting;
import gov.nj.innovation.customAwsIdp.keys.KeyConstants;
import gov.nj.innovation.customAwsIdp.keys.KeysWrapper;
import gov.nj.innovation.customAwsIdp.SamlGenerator;
Expand All @@ -15,6 +16,8 @@

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ssm.SsmClient;

/**
* Handler for getting a generated SAML Response, modified from code generated by Serverless ({@code
Expand Down Expand Up @@ -77,6 +80,10 @@ public class GetSamlResponseHandler implements RequestHandler<Map<String, Object
private static final String EMAIL_CLAIM = "email";
private static final String COGNITO_GROUPS_CLAIM = "cognito:groups";

private SsmClient ssmClient = SsmClient.builder()
.region(Region.of(System.getenv("COGNITO_REGION")))
.build();

@Override
public Map<String, String> handleRequest(Map<String, Object> input, Context context) {
final RequestParameters rp = extractRequestParametersFromInput(input);
Expand Down Expand Up @@ -111,7 +118,7 @@ public Map<String, String> handleRequest(Map<String, Object> input, Context cont

final KeyConstants keyConstants;
try {
keyConstants = new KeyConstants();
keyConstants = new KeyConstants(ssmClient);
} catch (NullPointerException | NumberFormatException e) {
return createErrorReturnMap(Status.SYSTEM_ERROR,
String.format("KeyConstants threw an exception: %s.\nNOTE: Please check that the " +
Expand Down Expand Up @@ -187,5 +194,10 @@ private Map<String, String> createReturnMap(
);
}

@VisibleForTesting
void setSsmClient(SsmClient ssmClient) {
this.ssmClient = ssmClient;
}

private enum Status { SUCCESS, INPUT_ERROR, SYSTEM_ERROR }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class CognitoGroupDescriptionMetadataExtractor {
* @param region AWS Region of the Cognito UserPool
* @param groupName Name of the Group whose description has the desired YAML metadata
* @param userPoolId ID of the UserPool to which the Group belongs
* @return {@link CognitoGroupDescriptionMetadata} representing the parsed YAML from the Group description
* @return {@link CognitoGroupDescriptionMetadata} representing the parsed YAML from the Group description.
*/
public static CognitoGroupDescriptionMetadata extract(String region, String groupName, String userPoolId) {
try (final CognitoIdentityProviderClient cognitoClient = CognitoIdentityProviderClient.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gov.nj.innovation.customAwsIdp.util;

import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
import software.amazon.awssdk.services.ssm.model.GetParameterResponse;

/**
* Fetch an SSM parameter by name.
*
* @author Case Walker ([email protected])
*/
public class SsmClientWrapper {

/**
* Encapsulate the Amazon types; consume a String and output its SSM value.
*
* @param parameterName An SSM parameter name
* @return The SSM parameter value.
*/
public static String getParameterByName(final SsmClient ssmClient, final String parameterName) {
GetParameterRequest request = GetParameterRequest.builder()
.name(parameterName)
.withDecryption(true)
.build();
GetParameterResponse response = ssmClient.getParameter(request);

return response.parameter().value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
import software.amazon.awssdk.services.ssm.model.GetParameterResponse;
import software.amazon.awssdk.services.ssm.model.Parameter;
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
import uk.org.webcompere.systemstubs.jupiter.SystemStub;

Expand Down Expand Up @@ -39,12 +43,12 @@ public class GetSamlResponseHandlerTest {
*/
@SystemStub
private final EnvironmentVariables envVariables = new EnvironmentVariables(
"KEY_PRIVATE_EXPONENT", "123",
"KEY_PRIME_P", "123",
"KEY_PRIME_Q", "123",
"KEY_PRIME_EXPONENT_P", "123",
"KEY_PRIME_EXPONENT_Q", "123",
"KEY_CRT_COEFFICIENT", "123",
"KEY_PRIVATE_EXPONENT_NAME", "custom-aws-idp-private-key-private-exponent",
"KEY_PRIME_P_NAME", "custom-aws-idp-private-key-prime-p",
"KEY_PRIME_Q_NAME", "custom-aws-idp-private-key-prime-q",
"KEY_PRIME_EXPONENT_P_NAME", "custom-aws-idp-private-key-prime-exponent-p",
"KEY_PRIME_EXPONENT_Q_NAME", "custom-aws-idp-private-key-prime-exponent-q",
"KEY_CRT_COEFFICIENT_NAME", "custom-aws-idp-private-key-crt-coefficient",
"COGNITO_REGION", "us-east-1",
"COGNITO_USER_POOL", "myPoolOfCoolUsers",
"DEFAULT_SESSION_DURATION", "900",
Expand All @@ -60,7 +64,7 @@ static void releaseStaticMock() {
@DisplayName("A well configured SamlGenerator results in a SUCCESS response with a SAMLResponse in the output")
void testSuccess() {
setupNiceGroupDescriptionExtractor();
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", "1234", EMAIL, "[resx-sandbox]"), null);

Expand All @@ -75,7 +79,7 @@ void testSuccess() {
@DisplayName("Still gets a SUCCESS response with a SAMLResponse in the output when 'duration' uses the default")
void testSuccessWithDefaultDuration() {
setupNiceGroupDescriptionExtractor();
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", null, EMAIL, "[resx-sandbox]"), null);

Expand All @@ -93,7 +97,7 @@ private static Stream<Arguments> testBadGroupName() {
@MethodSource
@DisplayName("Returns INPUT_ERROR with no SAMLResponse and an Error if groupName is null or blank")
void testBadGroupName(String groupName) {
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput(groupName, null, EMAIL, "[resx-sandbox]"), null);

Expand All @@ -109,7 +113,7 @@ void testBadGroupName(String groupName) {
@ValueSource(strings = { "-1", "1", "", "apple", "899", "43201" })
@DisplayName("Returns INPUT_ERROR with no SAMLResponse and an Error if duration is invalid")
void testBadDuration(String duration) {
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", duration, EMAIL, "[resx-sandbox]"), null);

Expand All @@ -128,7 +132,7 @@ private static Stream<Arguments> testBadEmail() {
@MethodSource
@DisplayName("Returns INPUT_ERROR with no SAMLResponse and an Error if Email is null or blank")
void testBadEmail(String email) {
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", null, email, "[resx-sandbox]"), null);

Expand All @@ -143,7 +147,7 @@ void testBadEmail(String email) {
@Test
@DisplayName("Returns INPUT_ERROR with no SAMLResponse and an Error if groupName is not in the user's groups")
void testNonConfiguredGroup() {
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();
Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", null, EMAIL, "[group1 group2]"), null);

Expand All @@ -161,7 +165,7 @@ void testYamlParserThrows() {
EXTRACTOR_MOCKER
.when(() -> CognitoGroupDescriptionMetadataExtractor.extract(anyString(), anyString(), anyString()))
.thenThrow(new RuntimeException("Outlook not so good"));
GetSamlResponseHandler getSamlResponseHandler = new GetSamlResponseHandler();
GetSamlResponseHandler getSamlResponseHandler = createNiceGetSamlResponseHandler();

Map<String, String> response = getSamlResponseHandler.handleRequest(
setupHandlerInput("resx-sandbox", null, EMAIL, "[resx-sandbox]"), null);
Expand Down Expand Up @@ -203,4 +207,23 @@ private void setupNiceGroupDescriptionExtractor() {
.when(() -> CognitoGroupDescriptionMetadataExtractor.extract(anyString(), anyString(), anyString()))
.thenReturn(new CognitoGroupDescriptionMetadata("https://test.com", "specialrole"));
}

private static class MockSsmClient implements SsmClient {
@Override
public String serviceName() { return "MockService"; }
@Override
public void close() {}
@Override
public GetParameterResponse getParameter(GetParameterRequest getParameterRequest) {
return GetParameterResponse.builder()
.parameter(Parameter.builder().name(getParameterRequest.name()).value("123").build())
.build();
}
}

private GetSamlResponseHandler createNiceGetSamlResponseHandler() {
GetSamlResponseHandler handler = new GetSamlResponseHandler();
handler.setSsmClient(new MockSsmClient());
return handler;
}
}

0 comments on commit c500277

Please sign in to comment.