diff --git a/README.md b/README.md index 57659ec..465948d 100644 --- a/README.md +++ b/README.md @@ -127,17 +127,20 @@ loginsvc: poll-time: 30min alg-name: "RS256" ``` -You AWS Secret must have the following values: -`PrivateKey` -`PublicKey` - -You will provide the keys for these values in the config. +Your AWS Secret must have at least 2 fields which correspond to the above properties: +``` +private-key-field-name: "privateKey" +public-key-field-name: "publicKey" +``` +with `"privateKey"` and `"publicKey"` indicating the field-name of those secrets. +Replace the above example values with the field-names you used in AWS Secrets Manager. There are a few important configuration values to be provided: - `access-exp-time` which indicates how long a token is valid for, - Optional property:`poll-time` which indicates how often key pairs (`private-key-field-name` and `public-key-field-name`) are polled and fetched from AWS Secrets Manager. Polling will be disabled if missing. - `alg-name` which indicates which algorithm is used to encode your keys. - Please note that only one configuration option (`loginsvc.rest.jwt.{aws-secrets-manager|generate-in-memory}`) can be used at a time. + +Please note that only one configuration option (`loginsvc.rest.jwt.{aws-secrets-manager|generate-in-memory}`) can be used at a time. ## How to generate Code coverage report ``` diff --git a/service/src/main/resources/example.application.yaml b/service/src/main/resources/example.application.yaml index 1807e56..9aa6916 100644 --- a/service/src/main/resources/example.application.yaml +++ b/service/src/main/resources/example.application.yaml @@ -5,7 +5,7 @@ loginsvc: #Configuration to generate the key in memory generate-in-memory: access-exp-time: 15min - rotation-time: 5min + rotation-time: 9h alg-name: "RS256" #Instead of generating the key in memory #The Below Config allows for the application to fetch keys from AWS Secrets Manager. diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/AwsSecretsManagerKeyConfig.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/AwsSecretsManagerKeyConfig.scala index 9c011c5..7e52356 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/AwsSecretsManagerKeyConfig.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/AwsSecretsManagerKeyConfig.scala @@ -88,8 +88,6 @@ case class AwsSecretsManagerKeyConfig (secretName: String, override def validate(): ConfigValidationResult = { - val defaultResults = defaultValidation - val awsSecretsResults = Seq( Option(secretName) .map(_ => ConfigValidationSuccess) @@ -101,15 +99,15 @@ case class AwsSecretsManagerKeyConfig (secretName: String, Option(privateKeyFieldName) .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("privateAwsKey is empty"))), + .getOrElse(ConfigValidationError(ConfigValidationException("privateKeyFieldName is empty"))), Option(publicKeyFieldName) .map(_ => ConfigValidationSuccess) - .getOrElse(ConfigValidationError(ConfigValidationException("publicAwsKey is empty"))), + .getOrElse(ConfigValidationError(ConfigValidationException("publicKeyFieldName is empty"))), ) val awsSecretsResultsMerge = awsSecretsResults.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge) - awsSecretsResultsMerge.merge(defaultResults) + super.validate().merge(awsSecretsResultsMerge) } } diff --git a/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/KeyConfig.scala b/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/KeyConfig.scala index 6e884e6..a053cf3 100644 --- a/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/KeyConfig.scala +++ b/service/src/main/scala/za/co/absa/loginsvc/rest/config/jwt/KeyConfig.scala @@ -44,7 +44,7 @@ trait KeyConfig extends ConfigValidatable { }) } - final def defaultValidation : ConfigValidationResult = { + override def validate(): ConfigValidationResult = { val algValidation = Try { SignatureAlgorithm.valueOf(algName) @@ -55,19 +55,19 @@ trait KeyConfig extends ConfigValidatable { case Failure(e) => throw e } - val accessExpTimeResult = if (accessExpTime < minAccessExpTime) { - ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least $minAccessExpTime")) + val accessExpTimeResult = if (accessExpTime < KeyConfig.minAccessExpTime) { + ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least ${KeyConfig.minAccessExpTime}")) } else ConfigValidationSuccess - val refreshKeyTimeResult = if (refreshKeyTime.nonEmpty && refreshKeyTime.get < minRefreshKeyTime) { - ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least $minRefreshKeyTime")) + val refreshKeyTimeResult = if (refreshKeyTime.nonEmpty && refreshKeyTime.get < KeyConfig.minRefreshKeyTime) { + ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least ${KeyConfig.minRefreshKeyTime}")) } else ConfigValidationSuccess algValidation.merge(accessExpTimeResult).merge(refreshKeyTimeResult) } +} - override def validate(): ConfigValidationResult = defaultValidation - +object KeyConfig { val minAccessExpTime: FiniteDuration = FiniteDuration(10, TimeUnit.MILLISECONDS) - val minRefreshKeyTime: FiniteDuration = FiniteDuration(5, TimeUnit.MINUTES) + val minRefreshKeyTime: FiniteDuration = FiniteDuration(10, TimeUnit.MILLISECONDS) } diff --git a/service/src/test/resources/application.yaml b/service/src/test/resources/application.yaml index 78cb557..2b7556f 100644 --- a/service/src/test/resources/application.yaml +++ b/service/src/test/resources/application.yaml @@ -4,7 +4,7 @@ loginsvc: jwt: generate-in-memory: access-exp-time: 15min - rotation-time: 9h + rotation-time: 5sec alg-name: "RS256" config: some-key: "BETA" diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/config/JwtConfigTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/config/JwtConfigTest.scala index 3554192..ecc9824 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/config/JwtConfigTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/config/JwtConfigTest.scala @@ -18,7 +18,7 @@ package za.co.absa.loginsvc.rest.config import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import za.co.absa.loginsvc.rest.config.jwt.{AwsSecretsManagerKeyConfig, InMemoryKeyConfig} +import za.co.absa.loginsvc.rest.config.jwt.{AwsSecretsManagerKeyConfig, InMemoryKeyConfig, KeyConfig} import za.co.absa.loginsvc.rest.config.validation.ConfigValidationException import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess} @@ -59,22 +59,22 @@ class JwtConfigTest extends AnyFlatSpec with Matchers { "inMemoryKeyConfig" should "fail on non-negative accessExpTime" in { inMemoryKeyConfig.copy(accessExpTime = FiniteDuration(5, TimeUnit.MILLISECONDS)).validate() shouldBe - ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least ${inMemoryKeyConfig.minAccessExpTime}")) + ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least ${KeyConfig.minAccessExpTime}")) } "awsSecretsManagerKeyConfig" should "fail on non-negative accessExpTime" in { awsSecretsManagerKeyConfig.copy(accessExpTime = FiniteDuration(5, TimeUnit.MILLISECONDS)).validate() shouldBe - ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least ${inMemoryKeyConfig.minAccessExpTime}")) + ConfigValidationError(ConfigValidationException(s"accessExpTime must be at least ${KeyConfig.minAccessExpTime}")) } "inMemoryKeyConfig" should "fail on non-negative refreshExpTime" in { inMemoryKeyConfig.copy(rotationTime = Option(FiniteDuration(5, TimeUnit.MILLISECONDS))).validate() shouldBe - ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least ${inMemoryKeyConfig.minRefreshKeyTime}")) + ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least ${KeyConfig.minRefreshKeyTime}")) } "awsSecretsManagerKeyConfig" should "fail on non-negative refreshExpTime" in { awsSecretsManagerKeyConfig.copy(pollTime = Option(FiniteDuration(5, TimeUnit.MILLISECONDS))).validate() shouldBe - ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least ${inMemoryKeyConfig.minRefreshKeyTime}")) + ConfigValidationError(ConfigValidationException(s"refreshKeyTime must be at least ${KeyConfig.minRefreshKeyTime}")) } "awsSecretsManagerKeyConfig" should "fail on missing value" in { diff --git a/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala b/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala index 6e6514a..fcfc341 100644 --- a/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala +++ b/service/src/test/scala/za/co/absa/loginsvc/rest/service/JWTServiceTest.scala @@ -150,4 +150,16 @@ class JWTServiceTest extends AnyFlatSpec { assert(jwk.getAlgorithm == JWSAlgorithm.RS256) assert(jwk.getKeyUse == KeyUse.SIGNATURE) } + + it should "rotate an public and private keys after 5 seconds" in { + val initToken = jwtService.generateToken(userWithoutGroups) + val initPublicKey = jwtService.publicKey + + Thread.sleep(5 * 1000) + val refreshedToken = jwtService.generateToken(userWithoutGroups) + + assert(parseJWT(initToken).isFailure) + assert(parseJWT(refreshedToken).isSuccess) + assert(initPublicKey != jwtService.publicKey) + } }