diff --git a/README.md b/README.md index 3665eac..e2b4b63 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Save both keys in a `private` folder (not under source control) and then point t secrets: keypair: private: "private/ec-key-1.pem" - pub: "private/ec-key-pub.pem" + pub: "private/ec-key.pub" ``` You can use either an absolute path, or the relative path to the current directory from where you are launching the Web server. diff --git a/aws-upload-keys.sh b/aws-upload-keys.sh index 6971917..535d751 100755 --- a/aws-upload-keys.sh +++ b/aws-upload-keys.sh @@ -3,22 +3,7 @@ # Copyright (c) 2021 AlertAvert.com. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# # Author: Marco Massenzio (marco@alertavert.com) -# - -# -# Generates a Elliptict Cryptography keypair using openssl set -eu @@ -72,15 +57,14 @@ out=$(mktemp /tmp/secret-XXXXXXXX.tmp) cat <$out { "priv": "$(while read -r line; do if [[ ! ${line} =~ ^----- ]]; \ - then echo -n ${line}; fi; done < ${PRIV})", - "pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; done < ${PUB})", - "algorithm": "EC" + then echo -n ${line}; fi; done < ${PRIV})", + "pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; \ + done < ${PUB})" } EOF - arn=$(aws ${ENDPOINT_URL} secretsmanager create-secret --name ${SECRET} \ - --description "Keypair ${KEY} generated by the $(basename $0) script" \ + --description "Elliptic Cryptography Keypair generated by the $(basename $0) script" \ --secret-string file://${out} | jq -r '.ARN') rm $out diff --git a/jwt-opa/src/main/java/com/alertavert/opa/Constants.java b/jwt-opa/src/main/java/com/alertavert/opa/Constants.java index f2609a1..a3ea215 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/Constants.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/Constants.java @@ -31,6 +31,7 @@ * All constants are grouped here for ease of reference. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) +@ExcludeFromCoverageGenerated public class Constants { /** * Basic Authorization header type. @@ -41,23 +42,12 @@ public class Constants { */ public static final String BEARER_TOKEN = "Bearer"; - /** - * The type of encryption accepted by the {@link com.alertavert.opa.jwt.JwtTokenProvider} - */ - public static final String ELLIPTIC_CURVE = "EC"; - /** Marker for a Public Key object */ public static final String PUBLIC_KEY = "PUBLIC KEY"; /** Marker for a Private Key object */ public static final String PRIVATE_KEY = "PRIVATE KEY"; - /** - * Passphrase-based encryption (see - * {@link com.alertavert.opa.configuration.KeyMaterialConfiguration}. - */ - public static final String PASSPHRASE = "SECRET"; - /** * The name of the Env Var which contains the name of the file storing the AWS API Token in a * running EKS container. @@ -129,4 +119,7 @@ public Collection getAuthorities() { }; public static final int MAX_TOKEN_LEN_LOG = 6; public static final ObjectMapper MAPPER = new ObjectMapper(); + public static final String PEM_EXT = ".pem"; + public static final String PUB_EXT = ".pub"; + } diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java index 607605a..416c317 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeyMaterialConfiguration.java @@ -18,58 +18,105 @@ package com.alertavert.opa.configuration; +import com.alertavert.opa.security.EnvSecretResolver; +import com.alertavert.opa.security.FileSecretResolver; +import com.alertavert.opa.security.NoopKeypairReader; +import com.alertavert.opa.security.NoopSecretResolver; +import com.alertavert.opa.security.SecretsResolver; +import com.alertavert.opa.security.aws.AwsSecretsKeypairReader; +import com.alertavert.opa.security.aws.AwsSecretsManagerResolver; +import com.alertavert.opa.security.crypto.KeyLoadException; +import com.alertavert.opa.security.crypto.KeypairFileReader; import com.alertavert.opa.security.crypto.KeypairReader; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import java.io.IOException; +import javax.annotation.PostConstruct; +import java.nio.file.Path; import java.security.KeyPair; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; -import static com.alertavert.opa.Constants.ELLIPTIC_CURVE; -import static com.alertavert.opa.Constants.PASSPHRASE; +import static com.alertavert.opa.Constants.PEM_EXT; +import static com.alertavert.opa.Constants.PUB_EXT; @Slf4j @Configuration -@EnableConfigurationProperties(TokensProperties.class) +@EnableConfigurationProperties({ + KeysProperties.class, + TokensProperties.class +}) +@RequiredArgsConstructor public class KeyMaterialConfiguration { - private final TokensProperties tokensProperties; + private final KeysProperties keyProperties; - public KeyMaterialConfiguration(TokensProperties properties) { - this.tokensProperties = properties; + /** + * AWS SecretsManager client, will only be configured by the + * {@link com.alertavert.opa.security.aws.AwsClientConfiguration AWS Configuration} if the + * {@literal aws} profile is active. + */ + @Autowired(required = false) + SecretsManagerClient secretsManagerClient; + + @PostConstruct + private void log() { + log.info("Configuring key material, algorithm = {}, location = {}", + keyProperties.algorithm, keyProperties.location); } @Bean - public String issuer() { - return tokensProperties.getIssuer(); + SecretsResolver secretsResolver() { + return switch (keyProperties.getLocation()) { + case env -> new EnvSecretResolver(); + case file -> new FileSecretResolver(); + case keypair -> new NoopSecretResolver(); + case awsSecret -> new AwsSecretsManagerResolver(secretsManagerClient); + case vaultPath -> throw new UnsupportedOperationException("Support for Vault not " + + "implemented yet"); + }; } @Bean - Algorithm hmac(KeypairReader reader) { - switch (reader.algorithm()) { - case PASSPHRASE: - return Algorithm.HMAC256(tokensProperties.getSecret()); - case ELLIPTIC_CURVE: - KeyPair keyPair = reader.loadKeys(); - return Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(), - (ECPrivateKey) keyPair.getPrivate()); - default: - throw new IllegalArgumentException(String.format("Algorithm [%s] not supported", - reader.algorithm())); - } + KeypairReader keypairReader() { + return switch (keyProperties.getLocation()) { + case env, file -> new NoopKeypairReader(); + case keypair -> new KeypairFileReader(keyProperties.algorithm.name(), + Path.of(keyProperties.name + PEM_EXT), Path.of(keyProperties.name + PUB_EXT)); + case awsSecret -> new AwsSecretsKeypairReader(secretsResolver(), keyProperties.name); + case vaultPath -> throw new UnsupportedOperationException("Support for Vault not " + + "implemented yet"); + }; } + @Bean - JWTVerifier verifier(Algorithm algorithm) throws IOException { - return JWT.require(algorithm) - .withIssuer(issuer()) - .build(); + Algorithm hmac(SecretsResolver resolver, KeypairReader reader) { + return switch (keyProperties.getAlgorithm()) { + case PASSPHRASE -> { + log.warn("Using insecure passphrase signing secret, name = {}", keyProperties.name); + String passphrase = resolver.getSecret(keyProperties.getName()).block(); + if (passphrase == null) { + log.error("Could not resolve secret {}, with SecretsResolver {}", + keyProperties.name, resolver.getClass().getSimpleName()); + throw new IllegalArgumentException("Signing secret cannot be resolved"); + } + yield Algorithm.HMAC256(passphrase); + } + case EC -> { + KeyPair keyPair = reader.loadKeys().block(); + if (keyPair == null) { + throw new KeyLoadException("Cannot load keypair " + keyProperties.name); + } + yield Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(), + (ECPrivateKey) keyPair.getPrivate()); + } + }; } } diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeysProperties.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeysProperties.java new file mode 100644 index 0000000..8be3607 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/KeysProperties.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

KeysPropertiers

+ * + * @author M. Massenzio, 202-11-19 + */ +@Data +@ConfigurationProperties(prefix = "keys") +public class KeysProperties { + + /** + *

Location for the signing secret

+ * + *

Possible values are: + * + *

  • {@literal env}: only available for PASSPHRASE, env var name which contains + * the signing secret
  • + *
  • {@literal file}: only valid if algorithm is PASSPHRASE, the file is simply read
  • + *
  • {@literal keypair}: the filename without extension, to which `.pem` and `.pub` + * will be added
  • + *
  • {@literal aws_secret}: name of AWS SecretsManager secret
  • + *
  • {@literal vault_path}: path in HashiCorp Vault
  • + * + *

    File paths can be absolute or relative. + * + *

    For a {@link AlgorithmType#PASSPHRASE passphrase}, the secret is simply read from + * SecretsManager/Vault, the {@link AlgorithmType#EC keypair} keypair is + * stored as a JSON-formatted secret, with two keys: "priv" and "pub" (see + * {@link com.alertavert.opa.security.aws.AwsSecretsKeypairReader.SecretKeys}). + */ + enum KeyLocation { + env, file, keypair, awsSecret, vaultPath + } + + public enum AlgorithmType { + /** Plaintext secret */ + PASSPHRASE, + /** Elliptic Curve cryptography */ + EC + } + + AlgorithmType algorithm; + KeyLocation location; + String name; +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/configuration/TokensProperties.java b/jwt-opa/src/main/java/com/alertavert/opa/configuration/TokensProperties.java index 614b59b..710859c 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/configuration/TokensProperties.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/configuration/TokensProperties.java @@ -22,7 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** - *

    KeyPropertiers

    + *

    TokensPropertiers

    * * @author M. Massenzio, 2020-12-14 */ diff --git a/jwt-opa/src/main/java/com/alertavert/opa/jwt/JwtTokenProvider.java b/jwt-opa/src/main/java/com/alertavert/opa/jwt/JwtTokenProvider.java index 4b2d03c..f4b42f9 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/jwt/JwtTokenProvider.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/jwt/JwtTokenProvider.java @@ -21,10 +21,10 @@ import com.alertavert.opa.configuration.TokensProperties; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; -import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.Nullable; @@ -56,15 +56,15 @@ public class JwtTokenProvider { @Autowired Algorithm hmac; - @Autowired - JWTVerifier verifier; - - @Autowired - String issuer; - @Autowired TokensProperties tokensProperties; + public JWTVerifier verifier() { + return JWT.require(hmac) + .withIssuer(tokensProperties.getIssuer()) + .build(); + } + /** *

    Creates a JWT token for the given user, signed with the private key of the issuer. * @@ -88,7 +88,7 @@ public String createToken(String user, List roles, @Nullable Instant exp log.debug("Issuing JWT for user = {}, roles = {}", user, roles); JWTCreator.Builder builder = JWT.create() - .withIssuer(issuer) + .withIssuer(tokensProperties.getIssuer()) .withSubject(user) .withIssuedAt(Date.from(now)) .withArrayClaim(ROLES, roles.toArray(new String[0])); @@ -131,7 +131,7 @@ public boolean validateToken(String token) { } public DecodedJWT decode(String token) throws JWTVerificationException { - return verifier.verify(token); + return verifier().verify(token); } public Authentication getAuthentication(String token) { diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/EnvSecretResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/EnvSecretResolver.java new file mode 100644 index 0000000..37f27d1 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/EnvSecretResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.security; + +import reactor.core.publisher.Mono; + +/** + *

    EnvSecretResolver

    + * + *

    Reads a secret from an environment variable. + * + *

    In all its triviality, this class is virtually untestable, due to + * the JVM limitations on setting test env vars (see {@literal EnvSecretReaderTest}). + * + * @author M. Massenzio, 2022-11-19 + */ +public class EnvSecretResolver implements SecretsResolver { + @Override + public Mono getSecret(String secretName) { + return Mono.justOrEmpty(System.getenv(secretName)); + } +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/FileSecretResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/FileSecretResolver.java new file mode 100644 index 0000000..ff7abe4 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/FileSecretResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.security; + +import lombok.SneakyThrows; +import reactor.core.publisher.Mono; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; + +/** + *

    FileSecretResolver

    + * + *

    Reads the secret from a file, if it exists. + * + * @author M. Massenzio, 2022-11-19 + */ +public class FileSecretResolver implements SecretsResolver { + @SneakyThrows + @Override + public Mono getSecret(String secretName) { + Path secretFile = Paths.get(secretName); + if (secretFile.toFile().exists()) { + BufferedReader reader = new + BufferedReader(new FileReader(secretFile.toFile())); + return Mono.just(reader.lines().collect(Collectors.joining())); + } + return Mono.empty(); + } +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/NoopKeypairReader.java b/jwt-opa/src/main/java/com/alertavert/opa/security/NoopKeypairReader.java new file mode 100644 index 0000000..44d2974 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/NoopKeypairReader.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.security; + +import com.alertavert.opa.ExcludeFromCoverageGenerated; +import com.alertavert.opa.security.crypto.KeyLoadException; +import com.alertavert.opa.security.crypto.KeypairReader; +import reactor.core.publisher.Mono; + +import java.security.KeyPair; + +/** + *

    NoopKeypairReader

    + * + *

    Placeholder for configurations where a keypair reader is not needed, but Spring still wants + * an injectable bean. + * + * @author M. Massenzio, 2022-11-20 + */ +@ExcludeFromCoverageGenerated +public class NoopKeypairReader implements KeypairReader { + @Override + public Mono loadKeys() throws KeyLoadException { + return Mono.empty(); + } +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/NoopSecretResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/NoopSecretResolver.java new file mode 100644 index 0000000..1751433 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/NoopSecretResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.security; + +import com.alertavert.opa.ExcludeFromCoverageGenerated; +import reactor.core.publisher.Mono; + +/** + *

    NoopSecretResolver

    + * + *

    Placeholder resolver, for when the secret configuration does not need one (e.g., for + * file-based keypair). + * + * @author M. Massenzio, 2022-11-19 + */ +@ExcludeFromCoverageGenerated +public class NoopSecretResolver implements SecretsResolver { + @Override + public Mono getSecret(String secretName) { + return Mono.empty(); + } +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java index 80f4670..1b57949 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java @@ -32,7 +32,6 @@ import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.authorization.AuthorizationContext; -import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.function.client.WebClient; @@ -40,7 +39,6 @@ import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; -import javax.sound.midi.Soundbank; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/SecretsResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/SecretsResolver.java new file mode 100644 index 0000000..8a69455 --- /dev/null +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/SecretsResolver.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 AlertAvert.com. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Author: Marco Massenzio (marco@alertavert.com) + */ + +package com.alertavert.opa.security; + +import reactor.core.publisher.Mono; + +/** + *

    Secrets Resolver

    + * + *

    Interface to resolve secrets in the application. + * + *

    Concrete classes will extract secrets from whatever store (local filesystem, env vars, + * Hashicorp Vault, AWS Secrets Manager) as configured and return to the caller. + */ +public interface SecretsResolver { + Mono getSecret(String secretName); +} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsClientConfiguration.java b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsClientConfiguration.java index 19e8b38..31fae50 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsClientConfiguration.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsClientConfiguration.java @@ -48,9 +48,6 @@ public class AwsClientConfiguration { @Value("${aws.endpoint:}") String endpoint; - @Value("${aws.keypair.secret_name:}") - private String keypairSecretName; - public Region region() { return Region.of(region); } @@ -94,13 +91,9 @@ public SecretsManagerClient secretsManagerClient() { // Typically this is overridden in tests, to point to a local Secrets Manager (e.g. using // LocalStack, pointing to http://localhost:4566) if (StringUtils.hasText(endpoint)) { + log.info("Using non-default endpoint, uri = {}", endpoint); builder.endpointOverride(URI.create(endpoint)); } return builder.build(); } - - @Bean - String keypairSecretName() { - return keypairSecretName; - } } diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReader.java b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReader.java index 3bdc084..df90cf9 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReader.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReader.java @@ -6,6 +6,7 @@ package com.alertavert.opa.security.aws; import com.alertavert.opa.ExcludeFromCoverageGenerated; +import com.alertavert.opa.security.SecretsResolver; import com.alertavert.opa.security.crypto.KeyLoadException; import com.alertavert.opa.security.crypto.KeypairReader; import com.alertavert.opa.thirdparty.PemUtils; @@ -26,6 +27,7 @@ import static com.alertavert.opa.Constants.KEYPAIR_LOADED; import static com.alertavert.opa.Constants.PRIVATE_KEY; import static com.alertavert.opa.Constants.PUBLIC_KEY; +import static com.alertavert.opa.configuration.KeysProperties.AlgorithmType.EC; /** *

    AwsSecretsKeypairReader

    @@ -44,7 +46,6 @@ public class AwsSecretsKeypairReader implements KeypairReader { */ @Data public static class SecretKeys { - String algorithm; String priv; String pub; } @@ -83,31 +84,22 @@ private Mono getSecret() { } @Override - public KeyPair loadKeys() throws KeyLoadException { - // TODO @MM: This is a bad pattern, blocking on a Reactive stream; we need to fix this API. + public Mono loadKeys() throws KeyLoadException { return getSecret() .map(keys -> new KeyPair(loadPublicKey(keys), loadPrivateKey(keys))) .doOnSuccess(kp -> log.info(KEYPAIR_LOADED, secretName)) .onErrorMap(KeyLoadException::new) .switchIfEmpty(Mono.error(new KeyLoadException("Cannot load keys from secret " + secretName))) - .doOnError(ex -> log.error(KEYPAIR_ERROR, secretName, ex.getMessage())) - .block(); - } - - @Override - public String algorithm() { - return getSecret() - .map(SecretKeys::getAlgorithm) - .block(); + .doOnError(ex -> log.error(KEYPAIR_ERROR, secretName, ex.getMessage())); } private PublicKey loadPublicKey(SecretKeys secretKeys) { PemObject object = new PemObject(PUBLIC_KEY, Base64.decode(secretKeys.pub)); - return PemUtils.getPublicKey(object.getContent(), secretKeys.algorithm); + return PemUtils.getPublicKey(object.getContent(), EC.name()); } private PrivateKey loadPrivateKey(SecretKeys secretKeys) { PemObject object = new PemObject(PRIVATE_KEY, Base64.decode(secretKeys.priv)); - return PemUtils.getPrivateKey(object.getContent(), secretKeys.algorithm); + return PemUtils.getPrivateKey(object.getContent(), EC.name()); } } diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java index 52c33ad..439ba61 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolver.java @@ -5,6 +5,7 @@ package com.alertavert.opa.security.aws; +import com.alertavert.opa.security.SecretsResolver; import com.alertavert.opa.security.crypto.KeyLoadException; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/SecretsResolver.java b/jwt-opa/src/main/java/com/alertavert/opa/security/aws/SecretsResolver.java deleted file mode 100644 index da90217..0000000 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/aws/SecretsResolver.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2021 AlertAvert.com. All rights reserved. - * Author: Marco Massenzio (marco@alertavert.com) - */ - -package com.alertavert.opa.security.aws; - -import reactor.core.publisher.Mono; - -/** - *

    Secrets Resolver

    - * - *

    Interface to resolve secrets in the application. - * - *

    Concrete classes will extract secrets from whatever store (local filesystem, env vars, - * Hashicorp Vault, AWS Secrets Manager) as configured and return to the caller. - */ -public interface SecretsResolver { - Mono getSecret(String secretName); -} diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairFileReader.java b/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairFileReader.java index 9ea1720..8038003 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairFileReader.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairFileReader.java @@ -18,11 +18,10 @@ package com.alertavert.opa.security.crypto; -import com.alertavert.opa.Constants; import com.alertavert.opa.thirdparty.PemUtils; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.slf4j.helpers.MessageFormatter; +import reactor.core.publisher.Mono; import java.io.IOException; import java.nio.file.Path; @@ -39,26 +38,22 @@ * match the one expected by the {@link #algorithm}; we use here the {@link PemUtils} utility * classes. * - *

    For an example as to how to generate a pair {@link Constants#ELLIPTIC_CURVE Elliptic Curve} - * cryptography key pair see the {@literal keygen.sh} script. + *

    For an example as to how to generate a pair of + * {@link com.alertavert.opa.configuration.KeysProperties.AlgorithmType#EC Elliptic Curve} + * cryptography keys, see the {@literal keygen.sh} script. * * @see PemUtils * @author M. Massenzio, 2022-01-24 */ -@Slf4j @Value -public class KeypairFileReader implements KeypairReader { - String algorithm; - Path secretKeyPath; - Path publicKeyPath; - - @Override - public KeyPair loadKeys() throws KeyLoadException { - return new KeyPair(loadPublicKey(), loadPrivateKey()); - } - +@Slf4j +public record KeypairFileReader( + String algorithm, + Path secretKeyPath, + Path publicKeyPath +) implements KeypairReader { @Override - public String algorithm() { - return algorithm; + public Mono loadKeys() throws KeyLoadException { + return Mono.just(new KeyPair(loadPublicKey(), loadPrivateKey())); } private PrivateKey loadPrivateKey() { @@ -72,7 +67,7 @@ private PrivateKey loadPrivateKey() { if (pk == null) { log.error(ERROR_CANNOT_READ_KEY, secretKeyPath, algorithm); throw new KeyLoadException( - MessageFormatter.format(ERROR_CANNOT_READ_KEY, secretKeyPath, algorithm).getMessage()); + MessageFormatter.format(ERROR_CANNOT_READ_KEY, secretKeyPath, algorithm).getMessage()); } log.info("Read private key, format: {}", pk.getFormat()); return pk; @@ -82,14 +77,14 @@ private PublicKey loadPublicKey() { log.info("Reading public key from file {}", publicKeyPath.toAbsolutePath()); PublicKey pk; try { - pk = PemUtils.readPublicKeyFromFile(publicKeyPath.toString(), algorithm); + pk = PemUtils.readPublicKeyFromFile(publicKeyPath.toString(), algorithm); } catch (IOException e) { throw new KeyLoadException(e); } if (pk == null) { log.error(ERROR_CANNOT_READ_KEY, publicKeyPath, algorithm); throw new KeyLoadException( - MessageFormatter.format(ERROR_CANNOT_READ_KEY, publicKeyPath, algorithm).getMessage()); + MessageFormatter.format(ERROR_CANNOT_READ_KEY, publicKeyPath, algorithm).getMessage()); } log.info("Read public key, format: {}", pk.getFormat()); return pk; diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairReader.java b/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairReader.java index d0aacc7..634e4af 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairReader.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/crypto/KeypairReader.java @@ -18,18 +18,18 @@ package com.alertavert.opa.security.crypto; +import reactor.core.publisher.Mono; + import java.security.KeyPair; /** *

    KeypairReader

    * *

    Classes implementing this interface will retrieve keys from their storage for use with the - * application, in a format compatible with the expected {@link #algorithm()}. + * application. * - * @author M. Massenzio, 2022-01-24 + * @author M. Massenzio, 2022-11-19 */ public interface KeypairReader { - KeyPair loadKeys() throws KeyLoadException; - - String algorithm(); + Mono loadKeys() throws KeyLoadException; } diff --git a/jwt-opa/src/test/java/com/alertavert/opa/AbstractTestBaseWithOpaContainer.java b/jwt-opa/src/test/java/com/alertavert/opa/AbstractTestBaseWithOpaContainer.java index afa7bc1..4b99fe0 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/AbstractTestBaseWithOpaContainer.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/AbstractTestBaseWithOpaContainer.java @@ -44,7 +44,7 @@ public static class Initializer implements ApplicationContextInitializerAwsSecretsKeypairReaderTest + * + *

    It is incredibly difficult (and, as of JDK 17, virtually impossible) to set an + * arbitrary env var, even for testing purposes; all the various hacks that used to + * work (usually via Reflection) have been essentially closed off. + * + *

    We use here the {@literal HOME} env var just because it is pretty much guaranteed + * to be defined in every environment, but it definitely makes this test flakier than we'd like + * to. + * + * @author M. Massenzio, 2022-10-28 + */ +@SpringBootTest(classes = { + KeyMaterialConfiguration.class, +}) +@ActiveProfiles(profiles = {"test"}) +@TestPropertySource(properties = { + "keys.algorithm = passphrase", + "keys.location = env", + "keys.name = HOME" +}) +class EnvSecretReaderTest { + + @Resource + KeyMaterialConfiguration configuration; + @Resource + Algorithm hmac; + + + @Test + public void getSecret() { + assertThat(configuration).isNotNull(); + assertThat(hmac).isNotNull(); + + SecretsResolver resolver = configuration.secretsResolver(); + assertThat(resolver.getClass()).isEqualTo(EnvSecretResolver.class); + assertThat(resolver.getSecret("HOME").block(Duration.ofMillis(5))).isEqualTo( + System.getenv("HOME")); + } +} diff --git a/jwt-opa/src/test/java/com/alertavert/opa/configuration/KeyMaterialConfigurationTest.java b/jwt-opa/src/test/java/com/alertavert/opa/configuration/KeyMaterialConfigurationTest.java index 208ff49..4341d70 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/configuration/KeyMaterialConfigurationTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/configuration/KeyMaterialConfigurationTest.java @@ -19,23 +19,16 @@ package com.alertavert.opa.configuration; import com.alertavert.opa.AbstractTestBase; -import com.alertavert.opa.Constants; -import com.alertavert.opa.jwt.JwtTokenProvider; +import com.alertavert.opa.security.NoopSecretResolver; +import com.alertavert.opa.security.SecretsResolver; import com.alertavert.opa.security.crypto.KeypairFileReader; import com.alertavert.opa.security.crypto.KeypairReader; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; -import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import java.io.IOException; -import java.nio.file.Paths; -import java.security.KeyPair; +import java.time.Duration; -import static com.alertavert.opa.Constants.ELLIPTIC_CURVE; import static org.assertj.core.api.Assertions.assertThat; class KeyMaterialConfigurationTest extends AbstractTestBase { @@ -43,38 +36,35 @@ class KeyMaterialConfigurationTest extends AbstractTestBase { @Autowired KeyMaterialConfiguration configuration; - @Value("${tokens.issuer}") - private String issuer; - @Autowired Algorithm hmac; + @Autowired + SecretsResolver resolver; + + @Autowired + KeypairReader keypairReader; + @Test - void issuer() { - assertThat(configuration.issuer()).isEqualTo(issuer); + void context() { + assertThat(hmac).isNotNull(); + assertThat(configuration).isNotNull(); + assertThat(keypairReader).isNotNull(); } @Test - void hmacGetsSecret() { - // This simply tests the injection works as intended. - assertThat(hmac.getName()).isEqualTo("HS256"); + void hmac() { + assertThat(hmac.getName()).isEqualTo("ES256"); } @Test - void verifier() throws IOException { - // We test here that we can get a JWT Verifier, using a Key pair, loaded from file. - KeypairReader fileReader = new KeypairFileReader(ELLIPTIC_CURVE, - Paths.get("../testdata/test.pem"), Paths.get("../testdata/test-pub.pem")); - Algorithm algo = configuration.hmac(fileReader); - JWTVerifier verifier = configuration.verifier(algo); - assertThat(verifier).isNotNull(); - - String token = JWT.create() - .withIssuer(issuer) - .withSubject("test-user") - .withClaim(JwtTokenProvider.ROLES, Lists.list("TEST")) - .sign(hmac); + void resolver() { + assertThat(resolver.getClass()).isEqualTo(NoopSecretResolver.class); + } - assertThat(verifier.verify(token)).isNotNull(); + @Test + void keypairReader() { + assertThat(keypairReader.getClass()).isEqualTo(KeypairFileReader.class); + assertThat(keypairReader.loadKeys().block(Duration.ofMillis(100))).isNotNull(); } } diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/ApiTokenAuthenticationTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/ApiTokenAuthenticationTest.java index aac94d5..4a50c43 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/ApiTokenAuthenticationTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/ApiTokenAuthenticationTest.java @@ -18,10 +18,10 @@ package com.alertavert.opa.security; -import com.alertavert.opa.jwt.JwtTokenProvider; -import com.auth0.jwt.interfaces.DecodedJWT; import com.alertavert.opa.AbstractTestBase; import com.alertavert.opa.jwt.ApiTokenAuthenticationFactory; +import com.alertavert.opa.jwt.JwtTokenProvider; +import com.auth0.jwt.interfaces.DecodedJWT; import org.assertj.core.util.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java index 03a4a36..3deb69d 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/OpaReactiveAuthorizationManagerTest.java @@ -50,7 +50,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/PasswordAuthenticationManagerTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/PasswordAuthenticationManagerTest.java index 5456004..dbda2cb 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/PasswordAuthenticationManagerTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/PasswordAuthenticationManagerTest.java @@ -30,7 +30,6 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; -import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java index 34ded7f..48b1a37 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/TokenBasedAuthorizationRequestTest.java @@ -18,7 +18,6 @@ package com.alertavert.opa.security; -import com.fasterxml.jackson.core.JsonProcessingException; import com.alertavert.opa.security.TokenBasedAuthorizationRequest.AuthRequestBody; import org.junit.jupiter.api.Test; diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReaderTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReaderTest.java index 7b758b5..ef01e10 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReaderTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsKeypairReaderTest.java @@ -53,8 +53,7 @@ class AwsSecretsKeypairReaderTest { String secret = """ { "priv": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYEIUVkTtwBilNcNoEzsP3jdslIlOtXQ5pByuzxhLJTChRANCAAS+gitL0EgxyFCvdT6rJ39DbCrLLwLReTA5OXahcIEeCBygfyh35H8T9r9uHszSOCpAk1QQMuhqURzyWEaKjk92", - "pub": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoIrS9BIMchQr3U+qyd/Q2wqyy8C0XkwOTl2oXCBHggcoH8od+R/E/a/bh7M0jgqQJNUEDLoalEc8lhGio5Pdg==", - "algorithm": "EC" + "pub": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoIrS9BIMchQr3U+qyd/Q2wqyy8C0XkwOTl2oXCBHggcoH8od+R/E/a/bh7M0jgqQJNUEDLoalEc8lhGio5Pdg==" } """; String priv = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYEIUVkTtwBilNcNoEzsP3jdslIlO" @@ -74,10 +73,9 @@ void setUp() { public void getSecret() { KeypairReader reader = new AwsSecretsKeypairReader( new AwsSecretsManagerResolver(secretsManagerClient), keypairName); - KeyPair keyPair = reader.loadKeys(); + KeyPair keyPair = reader.loadKeys().block(); assertNotNull(keyPair); assertThat(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded())) .isEqualTo(priv); - assertThat(reader.algorithm()).isEqualTo("EC"); } } diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolverTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolverTest.java index 3aa51a0..a55a938 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolverTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/aws/AwsSecretsManagerResolverTest.java @@ -18,15 +18,13 @@ package com.alertavert.opa.security.aws; -import com.alertavert.opa.JwtOpa; -import com.alertavert.opa.configuration.JwtSecurityConfiguration; import com.alertavert.opa.configuration.KeyMaterialConfiguration; -import com.alertavert.opa.configuration.OpaServerConfiguration; +import com.alertavert.opa.security.SecretsResolver; import com.alertavert.opa.security.crypto.KeyLoadException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ApplicationContextInitializer; @@ -35,14 +33,20 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.testcontainers.containers.localstack.LocalStackContainer; -import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.ResourceExistsException; +import java.net.URI; + +import static com.alertavert.opa.configuration.KeysProperties.AlgorithmType.EC; +import static com.alertavert.opa.configuration.KeysProperties.AlgorithmType.PASSPHRASE; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; /** *

    AwsSecretsManagerResolverTest

    @@ -51,18 +55,16 @@ */ @SpringBootTest(classes = { AwsClientConfiguration.class, + KeyMaterialConfiguration.class }) @ActiveProfiles(profiles = {"test", "aws"}) @ContextConfiguration(initializers = {AwsSecretsManagerResolverTest.Initializer.class}) class AwsSecretsManagerResolverTest { @Autowired - SecretsManagerClient client; - - @Autowired - String keypairSecretName; - - AwsSecretsManagerResolver resolver; + SecretsResolver resolver; + @Value("${keys.name}") + String secretName; public static class Initializer implements ApplicationContextInitializer { @Container @@ -73,36 +75,40 @@ public static class Initializer implements ApplicationContextInitializer resolver.getSecret("not-found").block()); + assertThrows(KeyLoadException.class, () -> resolver.getSecret("fake-name").block()); } } diff --git a/jwt-opa/src/test/java/com/alertavert/opa/security/crypto/KeypairFileReaderTest.java b/jwt-opa/src/test/java/com/alertavert/opa/security/crypto/KeypairFileReaderTest.java index 33e56f3..53930aa 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/security/crypto/KeypairFileReaderTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/security/crypto/KeypairFileReaderTest.java @@ -27,8 +27,9 @@ import java.nio.file.Paths; import java.security.KeyPair; +import static com.alertavert.opa.configuration.KeysProperties.AlgorithmType.EC; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; /** *

    KeypairFileReaderTest

    @@ -44,23 +45,23 @@ class KeypairFileReaderTest extends AbstractTestBase { @BeforeEach public void setup() { - reader = new KeypairFileReader("EC", + reader = new KeypairFileReader(EC.name(), Paths.get("testdata/test.pem"), Paths.get("testdata/test-pub.pem")); } @Test void loadKeys() { - KeyPair pair = reader.loadKeys(); + KeyPair pair = reader.loadKeys().block(); assertThat(pair).isNotNull(); assertThat(pair.getPrivate()).isNotNull(); assertThat(pair.getPublic()).isNotNull(); - assertThat(pair.getPublic().getAlgorithm()).isEqualTo("EC"); + assertThat(pair.getPublic().getAlgorithm()).isEqualTo(EC.name()); } @Test void nonExistKeysThrows() { - KeypairReader reader = new KeypairFileReader("EC", + KeypairReader reader = new KeypairFileReader(EC.name(), Paths.get("/etc/bogus/none.pub"), Paths.get("/etc/bogus/none.pem")); assertThrows(KeyLoadException.class, reader::loadKeys); } diff --git a/jwt-opa/src/test/java/com/alertavert/opa/thirdparty/PemUtilsTest.java b/jwt-opa/src/test/java/com/alertavert/opa/thirdparty/PemUtilsTest.java index e7fca52..6b909f3 100644 --- a/jwt-opa/src/test/java/com/alertavert/opa/thirdparty/PemUtilsTest.java +++ b/jwt-opa/src/test/java/com/alertavert/opa/thirdparty/PemUtilsTest.java @@ -18,7 +18,6 @@ package com.alertavert.opa.thirdparty; -import com.alertavert.opa.Constants; import org.junit.jupiter.api.Test; import java.io.FileNotFoundException; @@ -26,6 +25,7 @@ import java.security.PrivateKey; import java.security.PublicKey; +import static com.alertavert.opa.configuration.KeysProperties.AlgorithmType.EC; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -33,8 +33,8 @@ class PemUtilsTest { @Test public void readKeys() throws IOException { - PrivateKey pk = PemUtils.readPrivateKeyFromFile("testdata/test.pem", Constants.ELLIPTIC_CURVE); - PublicKey pubk = PemUtils.readPublicKeyFromFile("testdata/test-pub.pem", Constants.ELLIPTIC_CURVE); + PrivateKey pk = PemUtils.readPrivateKeyFromFile("testdata/test.pem", EC.name()); + PublicKey pubk = PemUtils.readPublicKeyFromFile("testdata/test-pub.pem", EC.name()); assertThat(pk).isNotNull(); assertThat(pubk).isNotNull(); } @@ -42,7 +42,7 @@ public void readKeys() throws IOException { @Test public void readKeysThrowsNotFound() throws IOException { assertThrows(FileNotFoundException.class, () -> - PemUtils.readPublicKeyFromFile("bogus.pem", Constants.ELLIPTIC_CURVE)); + PemUtils.readPublicKeyFromFile("bogus.pem", EC.name())); } @Test diff --git a/jwt-opa/src/test/resources/application-test.yaml b/jwt-opa/src/test/resources/application-test.yaml index 90d0605..fa68472 100644 --- a/jwt-opa/src/test/resources/application-test.yaml +++ b/jwt-opa/src/test/resources/application-test.yaml @@ -20,9 +20,45 @@ opa: headers: - "x-test-header" +# TODO: we should test more combinations of this configurations, but not sure how. +keys: + algorithm: ec + location: keypair + name: ../testdata/test + # Add an endpoint which won't trigger OPA Authorization routes: authenticated: - "/testauth" - "/match/*/this" - "/match/any/**" + +--- +# AWS Profile +spring: + config: + activate: + on-profile: aws + +# Use this with a locally running instance of localstack, configured to port 4566 +# Prior to running the webapp, upload the secret with: +# export AWS_REGION=us-west-2 +# export AWS_ENDPOINT=http://localhost:4566 +# aws --endpoint-url $AWS_ENDPOINT secretsmanager create-secret --name demo-secret \ +# --secret-string "astrong-secret-dce44st" +# +# To use a keypair instead, generate keys with keygen.sh, upload them with aws-upload-keys.sh +# (run those scripts with -h to see more details) and use: +# keys: +# algorithm: EC +# location: aws_secret +# name: demo-pair + +aws: + region: us-west-2 + profile: default + +keys: + algorithm: PASSPHRASE + location: aws_secret + name: test-secret diff --git a/keygen.sh b/keygen.sh index 8025858..b5c26d2 100755 --- a/keygen.sh +++ b/keygen.sh @@ -29,7 +29,7 @@ function usage { DIR optionally, a directory where to store the keys. -This script generates a private (KEY.pem) and public (KEY.pub) pair using openssl; +This script generates a private (KEY.pem) and public (KEY.pub) keypair using openssl; the keys are generated using Elliptic Cryptography. See also: https://github.com/auth0/java-jwt/issues/270 @@ -63,4 +63,4 @@ openssl ec -in ${PRIV} -pubout -out ${PUB} rm ${KEY}-param.pem -echo "[SUCCESS] Key Pair generated: ${PRIV} / ${PUB}" +echo "[SUCCESS] Key Pair generated: ${PRIV}, ${PUB}" diff --git a/run-example.sh b/run-example.sh index 859c052..df5384f 100755 --- a/run-example.sh +++ b/run-example.sh @@ -1,23 +1,12 @@ #!/usr/bin/env bash # # Copyright (c) 2021 AlertAvert.com. All rights reserved. -# # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# http://www.apache.org/licenses/LICENSE-2.0 # # Author: Marco Massenzio (marco@alertavert.com) -# Running the service with the containers it relies on. -set -eux +set -eu WORKDIR=$(dirname $0) diff --git a/testdata/test-key.pem b/testdata/test-key.pem deleted file mode 100644 index 4d82d2a..0000000 --- a/testdata/test-key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYEIUVkTtwBilNcNo -EzsP3jdslIlOtXQ5pByuzxhLJTChRANCAAS+gitL0EgxyFCvdT6rJ39DbCrLLwLR -eTA5OXahcIEeCBygfyh35H8T9r9uHszSOCpAk1QQMuhqURzyWEaKjk92 ------END PRIVATE KEY----- diff --git a/testdata/test-key.pub b/testdata/test-key.pub deleted file mode 100644 index 2a7f034..0000000 --- a/testdata/test-key.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoIrS9BIMchQr3U+qyd/Q2wqyy8C -0XkwOTl2oXCBHggcoH8od+R/E/a/bh7M0jgqQJNUEDLoalEc8lhGio5Pdg== ------END PUBLIC KEY----- diff --git a/testdata/test.pem b/testdata/test.pem new file mode 100644 index 0000000..82bad49 --- /dev/null +++ b/testdata/test.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjF8EEPC5DFnne+O5 +pa6hxeaNim4Ba9JH4E2K8jU3V92hRANCAAS0QnwnXncKy9x5//eR6UibMT+F1SRb +IFOKUSB+mhh/ChwtVXC5idc4Y4MHjTkItsGFkF08WoIk22DjM1gPx6Hx +-----END PRIVATE KEY----- diff --git a/testdata/test.pub b/testdata/test.pub new file mode 100644 index 0000000..1186a6f --- /dev/null +++ b/testdata/test.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtEJ8J153Csvcef/3kelImzE/hdUk +WyBTilEgfpoYfwocLVVwuYnXOGODB405CLbBhZBdPFqCJNtg4zNYD8eh8Q== +-----END PUBLIC KEY----- diff --git a/webapp-example/src/main/java/com/alertavert/opademo/JwtDemoApplication.java b/webapp-example/src/main/java/com/alertavert/opademo/JwtDemoApplication.java index a33c721..2410a61 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/JwtDemoApplication.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/JwtDemoApplication.java @@ -18,6 +18,7 @@ package com.alertavert.opademo; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,9 +31,16 @@ @EnableReactiveMongoRepositories(basePackages = "com.alertavert.opademo") @ComponentScan(basePackages = {"com.alertavert.opa", "com.alertavert.opademo"}) @EnableSwagger2 +@Slf4j public class JwtDemoApplication { public static void main(String[] args) { - SpringApplication.run(JwtDemoApplication.class, args); + try { + SpringApplication.run(JwtDemoApplication.class, args); + } catch (Exception ex) { + // Suppresses the insane amount of stacktrace Spring emits, and only logs the + // cause of the error. + log.error("Could not start application: {}", ex.getMessage()); + } } } diff --git a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java index a691fb2..f95f367 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java @@ -48,13 +48,10 @@ public class JwtController { JwtTokenProvider provider; ReactiveUsersRepository repository; - KeyPair keyPair; - public JwtController(JwtTokenProvider provider, ReactiveUsersRepository repository, - KeypairReader reader) { + public JwtController(JwtTokenProvider provider, ReactiveUsersRepository repository) { this.provider = provider; this.repository = repository; - keyPair = reader.loadKeys(); } @Data @@ -106,13 +103,4 @@ public Mono> authenticate( apiToken.substring(BEARER_TOKEN.length() + 1), user))); } - - /** - * "Demo" endpoint only accessible to SYSTEM administrators. - */ - // TODO: implement the Rego policy to only allow SYSTEM role to acces this API - @GetMapping(path = "/keypair", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) - public Mono> getKeypair() { - return Mono.just(ResponseEntity.ok(keyPair)); - } } diff --git a/webapp-example/src/main/java/com/alertavert/opademo/configuration/SecurityConfiguration.java b/webapp-example/src/main/java/com/alertavert/opademo/configuration/SecurityConfiguration.java index c2f00db..0dec2b3 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/configuration/SecurityConfiguration.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/configuration/SecurityConfiguration.java @@ -18,13 +18,9 @@ package com.alertavert.opademo.configuration; -import com.alertavert.opa.configuration.TokensProperties; -import com.alertavert.opa.security.crypto.KeypairFileReader; -import com.alertavert.opa.security.crypto.KeypairReader; import com.alertavert.opademo.data.ReactiveUsersRepository; import com.alertavert.opademo.data.User; import lombok.Data; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -35,8 +31,6 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsConfigurationSource; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import static com.alertavert.opa.Constants.EMPTY_USERDETAILS; @@ -58,11 +52,9 @@ public class SecurityConfiguration { public static final String DEFAULT_ALL_ALLOWED = "*"; private final CorsProperties properties; - private final KeyProperties keyProperties; - public SecurityConfiguration(CorsProperties properties, KeyProperties keyProperties) { + public SecurityConfiguration(CorsProperties properties) { this.properties = properties; - this.keyProperties = keyProperties; } @Data @@ -121,38 +113,4 @@ public CorsConfigurationSource corsConfiguration() { return conf; }; } - - /** - * Default key pair reader from the file system; to load a key pair from a different storage - * (e.g., Vault) implement your custom {@link KeypairReader} and inject it as a {@literal - * reader} bean. - * - *

    This reader will interpret the {@literal keys.priv,pub} properties as paths. - * - *

    To use your custom {@link KeypairReader} implementation, define your bean as primary: - * - //@formatter:off -

    -   @Bean @Primary
    -   public KeypairReader reader() {
    -     return new KeypairReader() {
    -       @Override
    -      public KeyPair loadKeys() throws KeyLoadException {
    -          // do something here
    -          return someKeypair;
    -      }
    -   };
    -   
    - //@formatter:on - * - * @return a reader which will try and load the key pair from the filesystem. - */ - @Bean - public KeypairReader filereader() { - Path priv = Paths.get(keyProperties.getPriv()).toAbsolutePath(); - Path pub = Paths.get(keyProperties.getPub()).toAbsolutePath(); - - log.info("Loading keys from {} and {}", priv, pub); - return new KeypairFileReader(keyProperties.getAlgorithm(), priv, pub); - } } diff --git a/webapp-example/src/main/resources/application.yaml b/webapp-example/src/main/resources/application.yaml index e5b58b9..7aecc0d 100644 --- a/webapp-example/src/main/resources/application.yaml +++ b/webapp-example/src/main/resources/application.yaml @@ -76,13 +76,29 @@ tokens: not_before_delay_sec: 0 keys: - # Elliptic Curve Signature algorithm - algorithm: "EC" - # Relative paths to a pair of private/public keys - # used to sign/verify the API Token (JWTs). - # See README.md for more information on how to generate them. - priv: "../private/ec-key.pem" - pub: "../private/ec-key-pub.pem" + # Signature algorithm + # + # Possible values are: + # PASSPHRASE: plaintext secret + # EC: Elliptic Curve cryptography key pair + algorithm: EC + + # Location for the signing secret + # + # Possible values for `location` are (with respective meaning for the `name` property): + # only available for PASSPHRASE + # env: env var name which contains the signing secret + # file: the file whose contents are the plaintext secret (NOT secure) + # + # keypair: the filename without extension, to which `.pem` and `.pub` will be added + # aws_secret: name of AWS SecretsManager secret + # vault_path: path in HashiCorp Vault + # + # File paths can be absolute or relative. + # For a PASSPHRASE, the secret is simply read from SecretsManager/Vault + # The keypair is stored as a JSON-formatted secret, with two keys: "priv" and "pub". + location: keypair + name: ../private/ec-key-1 logging: level: @@ -134,3 +150,46 @@ cors: - "http://*.example.com" - "http://*.alertavert.com:*" - "http://localhost:*" + +--- +# AWS Profile +spring: + config: + activate: + on-profile: aws + +# Use this with a locally running instance of localstack, configured to port 4566 +# Prior to running the webapp, upload the secret with: +# export AWS_REGION=us-west-2 +# export AWS_ENDPOINT=http://localhost:4566 +# aws --endpoint-url $AWS_ENDPOINT secretsmanager create-secret --name demo-secret \ +# --secret-string "astrong-secret-dce44st" +# +# To use a keypair instead, generate keys with keygen.sh, upload them with aws-upload-keys.sh +# (run those scripts with -h to see more details) and use: +# keys: +# algorithm: EC +# location: aws_secret +# name: demo-pair + +aws: + region: us-west-2 + profile: default + endpoint: http://localhost:4566 + +keys: + algorithm: PASSPHRASE + location: aws_secret + name: demo-secret +--- +# Example setting the signing secret via an env var + +spring: + config: + activate: + on-profile: env + +keys: + algorithm: PASSPHRASE + location: env + name: JWT_SIGNING_ENV diff --git a/webapp-example/src/test/resources/application-test.yaml b/webapp-example/src/test/resources/application-test.yaml index a4fef31..7bc6a23 100644 --- a/webapp-example/src/test/resources/application-test.yaml +++ b/webapp-example/src/test/resources/application-test.yaml @@ -20,9 +20,9 @@ tokens: not_before_delay_sec: 15 keys: - algorithm: "EC" - priv: "../testdata/test-key.pem" - pub: "../testdata/test-key.pub" + algorithm: ec + location: keypair + name: ../testdata/test # This is used to build the Rule validation endpoint: