From 7b3db20a28261ca796b2ff809654e41fb06fc75a Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 28 Jul 2024 04:01:40 +0100 Subject: [PATCH 01/15] feat: update test and improve code redability --- build.sbt | 43 +++++--- .../main/scala/io/oath/circe/conversion.scala | 12 +++ .../src/main/scala/io/oath/circe/syntax.scala | 2 +- .../src/test/scala/io/oath/circe/Bar.scala | 6 +- .../io/oath/circe/CirceConversionSpec.scala | 25 ++++- .../src/test/scala/io/oath/circe/Foo.scala | 4 +- .../src/main/scala/io/oath/Jwt.scala | 2 +- .../src/main/scala/io/oath/JwtClaims.scala | 7 +- .../main/scala/io/oath/JwtIssueError.scala | 13 +-- .../src/main/scala/io/oath/JwtIssuer.scala | 26 +++-- .../src/main/scala/io/oath/JwtManager.scala | 3 +- .../src/main/scala/io/oath/JwtVerifier.scala | 41 ++++---- .../main/scala/io/oath/JwtVerifyError.scala | 22 +++-- .../scala/io/oath/test/JwtIssuerSpec.scala | 47 ++++++--- .../scala/io/oath/test/JwtVerifierSpec.scala | 99 ++++++++++++------- .../io/oath/jsoniter_scala/conversion.scala | 8 ++ .../scala/io/oath/jsoniter_scala/Bar.scala | 4 +- .../JsoniterConversionSpec.scala | 19 +++- project/Projects.scala | 17 +--- 19 files changed, 255 insertions(+), 145 deletions(-) create mode 100644 modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala create mode 100644 modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala diff --git a/build.sbt b/build.sbt index 3ef3bf1..b5ea877 100644 --- a/build.sbt +++ b/build.sbt @@ -50,34 +50,53 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( ), ) -lazy val root = Projects - .createModule("oath", ".") +ThisBuild / Test / fork := true +ThisBuild / run / fork := true +ThisBuild / Test / parallelExecution := false +ThisBuild / scalafmtOnCompile := sys.env.getOrElse("RUN_SCALAFMT_ON_COMPILE", "false").toBoolean +ThisBuild / scalafixOnCompile := sys.env.getOrElse("RUN_SCALAFIX_ON_COMPILE", "false").toBoolean +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := "4.8.15" + +def rootModule(rootModule: String)(subModule: String): Project = + Project(s"$rootModule-$subModule", file(s"$rootModule${if (subModule == "root") "" else s"/$subModule"}")) + +lazy val root = Project("oath", file(".")) .enablePlugins(NoPublishPlugin) .settings(Aliases.all) - .aggregate(modules *) + .aggregate(allModules *) + +lazy val example = project + .in(file("example")) + .dependsOn(oathCore) + +val createOathModule = rootModule("oath") _ -lazy val oathMacros = Projects - .createModule("oath-macros", "modules/oath-macros") +lazy val oathRoot = createOathModule("root") + .aggregate(oathModules *) + +lazy val oathMacros = createOathModule("macros") .settings(Dependencies.oathMacros) -lazy val oathCore = Projects - .createModule("oath-core", "modules/oath-core") +lazy val oathCore = createOathModule("core") .dependsOn(oathMacros) .settings(Dependencies.oathCore) -lazy val oathCirce = Projects - .createModule("oath-circe", "modules/oath-circe") +lazy val oathCirce = createOathModule("circe") .settings(Dependencies.oathCirce) .dependsOn(oathCore % "compile->compile;test->test") -lazy val oathJsoniterScala = Projects - .createModule("oath-jsoniter-scala", "modules/oath-jsoniter-scala") +lazy val oathJsoniterScala = createOathModule("jsoniter-scala") .settings(Dependencies.oathJsoniterScala) .dependsOn(oathCore % "compile->compile;test->test") -lazy val modules: Seq[ProjectReference] = Seq( +lazy val oathModules: Seq[ProjectReference] = Seq( oathMacros, oathCore, oathCirce, oathJsoniterScala, ) + +lazy val exampleModules: Seq[ProjectReference] = Seq(example) + +lazy val allModules: Seq[ProjectReference] = exampleModules ++ oathModules diff --git a/modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala b/modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala new file mode 100644 index 0000000..d1b45d0 --- /dev/null +++ b/modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala @@ -0,0 +1,12 @@ +package io.oath.circe + +import io.circe.* +import io.oath.circe.syntax.* +import io.oath.json.* + +object conversion: + given [P](using encoder: Encoder[P]): ClaimsEncoder[P] = encoder.convert + + given [P](using decoder: Decoder[P]): ClaimsDecoder[P] = decoder.convert + + given [P](using codec: Codec[P]): ClaimsCodec[P] = codec.convertCodec diff --git a/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala b/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala index 8dd54ed..3102a2f 100644 --- a/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala +++ b/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala @@ -1,6 +1,6 @@ package io.oath.circe -import io.circe._ +import io.circe.* import io.circe.syntax.EncoderOps import io.oath.JwtVerifyError import io.oath.json.{ClaimsCodec, ClaimsDecoder, ClaimsEncoder} diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala b/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala index a8c79c7..6b3c00c 100644 --- a/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala +++ b/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala @@ -2,11 +2,9 @@ package io.oath.circe import io.circe.generic.semiauto.* import io.circe.{Decoder, Encoder} -import io.oath.circe.syntax.* -import io.oath.json.* case class Bar(name: String, age: Int) object Bar: - given barEncoder: ClaimsEncoder[Bar] = deriveEncoder[Bar].convert - given barDecoder: ClaimsDecoder[Bar] = deriveDecoder[Bar].convert + given barEncoder: Encoder[Bar] = deriveEncoder[Bar] + given barDecoder: Decoder[Bar] = deriveDecoder[Bar] diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala index b6c1dd2..40b5dd7 100644 --- a/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala +++ b/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala @@ -3,12 +3,15 @@ package io.oath.circe import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.oath.* +import io.oath.circe.conversion.given import io.oath.config.JwtIssuerConfig.RegisteredConfig import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} import io.oath.config.* +import io.oath.json.ClaimsDecoder import io.oath.syntax.* import io.oath.testkit.AnyWordSpecBase import io.oath.utils.CodecUtils +import org.typelevel.jawn.ParseException class CirceConversionSpec extends AnyWordSpecBase, CodecUtils: @@ -19,6 +22,7 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils: ProvidedWithConfig(None, None, Nil), LeewayWindowConfig(None, None, None, None), ) + val issuerConfig = JwtIssuerConfig( Algorithm.HMAC256("secret"), @@ -29,22 +33,24 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils: val jwtVerifier = new JwtVerifier(verifierConfig) val jwtIssuer = new JwtIssuer(issuerConfig) - "CirceConversion" should: - "convert circe (encoders & decoders) to claims (encoders & decoders)" in: + "CirceConversion" should { + "convert circe (encoders & decoders) to claims (encoders & decoders)" in { val bar = Bar("bar", 10) val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(bar)).value val claims = jwtVerifier.verifyJwt[Bar](jwt.token.toTokenP).value claims.payload shouldBe bar + } - "convert circe (codec) to claims (encoders & decoders)" in: + "convert circe (codec) to claims (encoders & decoders)" in { val foo = Foo("foo", 10) val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("issuer")))).value val claims = jwtVerifier.verifyJwt[Foo](jwt.token.toTokenP).value claims.payload shouldBe foo + } - "convert circe decoder to claims decoder and get error" in: + "convert circe decoder to claims decoder and get error" in { val fooJson = """{"name":"Hello","age":"not number"}""" val jwt = JWT .create() @@ -53,3 +59,14 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils: val claims = jwtVerifier.verifyJwt[Foo](jwt.toTokenP) claims.left.value shouldBe JwtVerifyError.DecodingError("DecodingFailure at .age: Int", null) + } + + "convert circe decoder to claims decoder and get error when format is incorrect" in { + val fooJson = """{"name":,}""" + + summon[ClaimsDecoder[Foo]].decode(fooJson).left.value shouldEqual JwtVerifyError.DecodingError( + "expected json value got ',}' (line 1, column 9)", + ParseException("expected json value got ',}' (line 1, column 9)", 8, 1, 9), + ) + } + } diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala b/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala index d63d439..7baa3cc 100644 --- a/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala +++ b/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala @@ -2,10 +2,8 @@ package io.oath.circe import io.circe.Codec import io.circe.generic.semiauto.* -import io.oath.circe.syntax.* -import io.oath.json.ClaimsCodec final case class Foo(name: String, age: Int) object Foo: - given barCodec: ClaimsCodec[Foo] = deriveCodec[Foo].convertCodec + given barCodec: Codec[Foo] = deriveCodec[Foo] diff --git a/modules/oath-core/src/main/scala/io/oath/Jwt.scala b/modules/oath-core/src/main/scala/io/oath/Jwt.scala index 13065f0..9a433c4 100644 --- a/modules/oath-core/src/main/scala/io/oath/Jwt.scala +++ b/modules/oath-core/src/main/scala/io/oath/Jwt.scala @@ -1,3 +1,3 @@ package io.oath -case class Jwt[T <: JwtClaims](claims: T, token: String) +final case class Jwt[T <: JwtClaims](claims: T, token: String) diff --git a/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala b/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala index 343affb..93e6672 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala @@ -1,11 +1,9 @@ package io.oath -sealed trait JwtClaims { +sealed trait JwtClaims: val registered: RegisteredClaims -} - -object JwtClaims { +object JwtClaims: final case class Claims(registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims final case class ClaimsH[+H](header: H, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims @@ -14,4 +12,3 @@ object JwtClaims { final case class ClaimsHP[+H, +P](header: H, payload: P, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims -} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala b/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala index 01ab908..d1eda0e 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala @@ -1,10 +1,11 @@ package io.oath -sealed abstract class JwtIssueError(val error: String) extends Exception(error) +sealed abstract class JwtIssueError(error: String, cause: Throwable = null) extends Exception(error, cause) object JwtIssueError: - case class IllegalArgument(override val error: String) extends JwtIssueError(error) - case class JwtCreationIssueError(override val error: String) extends JwtIssueError(error) - case class EncryptionError(override val error: String) extends JwtIssueError(error) - case class EncodeError(override val error: String) extends JwtIssueError(error) - case class UnexpectedIssueError(override val error: String) extends JwtIssueError(error) + case class IllegalArgument(message: String, underlying: Throwable) extends JwtIssueError(message, underlying) + case class JwtCreationIssueError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying) + case class EncryptionError(message: String) extends JwtIssueError(message) + case class EncodeError(message: String) extends JwtIssueError(message) + case class UnexpectedIssueError(message: String, underlying: Option[Throwable] = None) + extends JwtIssueError(message, underlying.orNull) diff --git a/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala b/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala index fed7236..9baaf09 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala @@ -26,7 +26,7 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) - private def setRegisteredClaims(adHocRegisteredClaims: RegisteredClaims): RegisteredClaims = + private def setRegisteredClaims(adHocRegisteredClaims: RegisteredClaims): RegisteredClaims = { val now = Instant.now(clock).truncatedTo(ChronoUnit.SECONDS) RegisteredClaims( iss = adHocRegisteredClaims.iss orElse config.registered.issuerClaim, @@ -47,6 +47,7 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) .pipe(prefix => prefix + UUID.randomUUID().toString) ), ) + } private def maybeEncryptJwt[T <: JwtClaims]( jwt: Jwt[T] @@ -64,14 +65,17 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) .withTry(builder.sign(algorithm)) .toEither .left - .map: - case e: IllegalArgumentException => JwtIssueError.IllegalArgument(e.getMessage) - case e: JWTCreationException => JwtIssueError.JwtCreationIssueError(e.getMessage) - case e => JwtIssueError.UnexpectedIssueError(e.getMessage) + .map { + case e: IllegalArgumentException => + JwtIssueError.IllegalArgument("JwtIssuer failed with IllegalArgumentException", e) + case e: JWTCreationException => + JwtIssueError.JwtCreationIssueError("JwtIssuer failed with JWTCreationException", e) + case e => JwtIssueError.UnexpectedIssueError("JwtIssuer failed with unexpected exception", Some(e)) + } def issueJwt( claims: JwtClaims.Claims = JwtClaims.Claims() - ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = + ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = { val jwtBuilder = JWT.create() setRegisteredClaims(claims.registered) .pipe(registeredClaims => buildJwt(jwtBuilder, registeredClaims) -> registeredClaims) @@ -85,10 +89,11 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) } .flatMap(jwt => maybeEncryptJwt(jwt)) + } def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using ClaimsEncoder[H] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = { val jwtBuilder = JWT.create() for headerBuilder <- jwtBuilder.safeEncodeHeader(claims.header) @@ -101,10 +106,11 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) encryptedJwt <- maybeEncryptJwt(jwt) yield encryptedJwt + } def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using ClaimsEncoder[P] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = { val jwtBuilder = JWT.create() for payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) @@ -117,10 +123,11 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) encryptedJwt <- maybeEncryptJwt(jwt) yield encryptedJwt + } def issueJwt[H, P]( claims: JwtClaims.ClaimsHP[H, P] - )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = + )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = { val jwtBuilder = JWT.create() for payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) @@ -134,3 +141,4 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) encryptedJwt <- maybeEncryptJwt(jwt) yield encryptedJwt + } diff --git a/modules/oath-core/src/main/scala/io/oath/JwtManager.scala b/modules/oath-core/src/main/scala/io/oath/JwtManager.scala index 8b26dae..76c0354 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtManager.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtManager.scala @@ -3,7 +3,7 @@ package io.oath import io.oath.config.* import io.oath.json.{ClaimsDecoder, ClaimsEncoder} -final class JwtManager(config: JwtManagerConfig): +final class JwtManager(config: JwtManagerConfig) { private val issuer: JwtIssuer = new JwtIssuer(config.issuer) private val verifier: JwtVerifier = new JwtVerifier(config.verifier) @@ -38,3 +38,4 @@ final class JwtManager(config: JwtManagerConfig): jwt: JwtToken.TokenHP )(using ClaimsDecoder[H], ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = verifier.verifyJwt[H, P](jwt) +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala b/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala index d48620d..0777c2a 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala @@ -21,7 +21,7 @@ final class JwtVerifier(config: JwtVerifierConfig): .tap(jwtVerification => if (config.providedWith.audienceClaims.nonEmpty) jwtVerification.withAudience(config.providedWith.audienceClaims: _*) - () + else () ) .tap(jwtVerification => config.leewayWindow.leeway.map(duration => jwtVerification.acceptLeeway(duration.toSeconds)) @@ -37,7 +37,7 @@ final class JwtVerifier(config: JwtVerifierConfig): ) .build() - private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = + inline private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = RegisteredClaims( iss = decodedJWT.getOptionIssuer, sub = decodedJWT.getOptionSubject, @@ -48,41 +48,42 @@ final class JwtVerifier(config: JwtVerifierConfig): jti = decodedJWT.getOptionJwtID, ) - private def maybeDecryptJwt(token: String): Either[JwtVerifyError.DecryptionError, String] = + inline private def maybeDecryptJwt(token: String): Either[JwtVerifyError.DecryptionError, String] = config.encrypt .map(encryptionConfig => DecryptionUtils.decryptAES(token, encryptionConfig.secret)) .getOrElse(Right(token)) - private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = - Option(token).filter(_.nonEmpty).toRight(JwtVerifyError.VerificationError("JWT Token is empty.")) + inline private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = + Option(token) + .filter(_.nonEmpty) + .toRight(JwtVerifyError.VerificationError("JWTVerifier failed with an empty token.")) - private def safeDecode[T]( + inline private def safeDecode[T]( decodedObject: => Either[JwtVerifyError.DecodingError, T] ): Either[JwtVerifyError.DecodingError, T] = allCatch .withTry(decodedObject) .fold(error => Left(JwtVerifyError.DecodingError(error.getMessage, error)), identity) - private def handler(decodedJWT: => DecodedJWT): Either[JwtVerifyError, DecodedJWT] = + inline private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = allCatch - .withTry(decodedJWT) + .withTry(jwtVerifier.verify(token)) .toEither .left .map { - case e: IllegalArgumentException => JwtVerifyError.IllegalArgument(e.getMessage) - case e: AlgorithmMismatchException => JwtVerifyError.AlgorithmMismatch(e.getMessage) - case e: SignatureVerificationException => JwtVerifyError.SignatureVerificationError(e.getMessage) - case e: TokenExpiredException => JwtVerifyError.TokenExpired(e.getMessage) - case e: JWTVerificationException => JwtVerifyError.VerificationError(e.getMessage) - case e => JwtVerifyError.UnexpectedError(e.getMessage) + case e: IllegalArgumentException => + JwtVerifyError.IllegalArgument("JwtVerifier failed with IllegalArgumentException", e) + case e: AlgorithmMismatchException => + JwtVerifyError.AlgorithmMismatch("JwtVerifier failed with AlgorithmMismatchException", e) + case e: SignatureVerificationException => + JwtVerifyError.SignatureVerificationError("JwtVerifier failed with SignatureVerificationException", e) + case e: TokenExpiredException => + JwtVerifyError.TokenExpired("JwtVerifier failed with TokenExpiredException", Some(e)) + case e: JWTVerificationException => + JwtVerifyError.VerificationError("JwtVerifier failed with JWTVerificationException", Some(e)) + case e => JwtVerifyError.UnexpectedError("JwtVerifier failed with unexpected exception", Some(e)) } - private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = - handler( - jwtVerifier - .verify(token) - ) - def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = for token <- validateToken(jwt.token) diff --git a/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala b/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala index 4ee41e7..e102566 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala +++ b/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala @@ -1,13 +1,17 @@ package io.oath -sealed abstract class JwtVerifyError(val error: String) extends Exception(error) +sealed abstract class JwtVerifyError(error: String, cause: Throwable = null) extends Exception(error, cause) object JwtVerifyError: - case class IllegalArgument(override val error: String) extends JwtVerifyError(error) - case class AlgorithmMismatch(override val error: String) extends JwtVerifyError(error) - case class DecodingError(override val error: String, underlying: Throwable) extends JwtVerifyError(error) - case class VerificationError(override val error: String) extends JwtVerifyError(error) - case class SignatureVerificationError(override val error: String) extends JwtVerifyError(error) - case class DecryptionError(override val error: String) extends JwtVerifyError(error) - case class TokenExpired(override val error: String) extends JwtVerifyError(error) - case class UnexpectedError(override val error: String) extends JwtVerifyError(error) + case class IllegalArgument(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) + case class AlgorithmMismatch(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) + case class DecodingError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) + case class VerificationError(message: String, underlying: Option[Throwable] = None) + extends JwtVerifyError(message, underlying.orNull) + case class SignatureVerificationError(message: String, underlying: Throwable) + extends JwtVerifyError(message, underlying) + case class DecryptionError(message: String) extends JwtVerifyError(message) + case class TokenExpired(message: String, underlying: Option[Throwable] = None) + extends JwtVerifyError(message, underlying.orNull) + case class UnexpectedError(message: String, underlying: Option[Throwable] = None) + extends JwtVerifyError(message, underlying.orNull) diff --git a/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala b/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala index ce18dc7..f6d07f9 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala +++ b/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala @@ -22,9 +22,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: .acceptLeeway(5) .build() - "JwtIssuer" should: - "issue jwt tokens" when: - "issue token with predefine configure claims" in forAll: (config: JwtIssuerConfig) => + "JwtIssuer" should { + "issue jwt tokens" when { + "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => val now = getInstantNowSeconds val jwtIssuer = new JwtIssuer(config.copy(encrypt = None), getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt().value @@ -52,8 +52,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe config.registered.notBeforeOffset.map(offset => now.plusSeconds(offset.toSeconds) ) + } - "issue token with predefine configure claims and ad-hoc registered claims" in forAll: + "issue token with predefine configure claims and ad-hoc registered claims" in forAll { (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => val now = getInstantNowSeconds val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) @@ -85,8 +86,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: else if (config.registered.includeJwtIdClaim) jwtClaims.claims.registered.jti should not be empty else jwtClaims.claims.registered.jti shouldBe empty + } - "issue token with only registered claims empty strings" in forAll: + "issue token with only registered claims empty strings" in forAll { (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => val now = getInstantNowSeconds val adHocRegisteredClaims = @@ -106,8 +108,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } - "issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll: + "issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll { (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => val now = getInstantNowSeconds val adHocRegisteredClaims = @@ -127,8 +130,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } - "issue token with only registered claims encrypted" in forAll: + "issue token with only registered claims encrypted" in forAll { (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => whenever(config.encrypt.nonEmpty): val clock = getFixedClock(getInstantNowSeconds) @@ -137,8 +141,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" jwt.token.length % 16 shouldBe 0 + } - "issue token with header claims" in forAll: (config: JwtIssuerConfig, header: NestedHeader) => + "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) val jwt = jwtIssuer.issueJwt(header.toClaimsH).value @@ -151,16 +156,18 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: .value result shouldBe header + } - "issue token with header claims encrypted" in forAll: (config: JwtIssuerConfig, header: NestedHeader) => + "issue token with header claims encrypted" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => whenever(config.encrypt.nonEmpty): val jwtIssuer = new JwtIssuer(config) val jwt = jwtIssuer.issueJwt(header.toClaimsH).value jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" jwt.token.length % 16 shouldBe 0 + } - "issue token with payload claims" in forAll: (config: JwtIssuerConfig, payload: NestedPayload) => + "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value @@ -173,16 +180,18 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: .value result shouldBe payload + } - "issue token with payload claims encrypted" in forAll: (config: JwtIssuerConfig, payload: NestedPayload) => + "issue token with payload claims encrypted" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => whenever(config.encrypt.nonEmpty): val jwtIssuer = new JwtIssuer(config) val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" jwt.token.length % 16 shouldBe 0 + } - "issue token with header & payload claims" in forAll: + "issue token with header & payload claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value @@ -198,8 +207,9 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: headerResult shouldBe header payloadResult shouldBe payload + } - "issue token with header & payload claims encrypted" in forAll: + "issue token with header & payload claims encrypted" in forAll { (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => whenever(config.encrypt.nonEmpty): val jwtIssuer = new JwtIssuer(config) @@ -207,10 +217,17 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" jwt.token.length % 16 shouldBe 0 + } - "issue token should fail with IllegalArgument when algorithm is set to null" in forAll: + "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { (config: JwtIssuerConfig) => val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) val jwt = jwtIssuer.issueJwt() - jwt.left.value shouldBe JwtIssueError.IllegalArgument("The Algorithm cannot be null.") + jwt.left.value shouldEqual JwtIssueError.IllegalArgument( + "JwtIssuer failed with IllegalArgumentException", + new IllegalArgumentException("Algorithm cannot be null"), + ) + } + } + } diff --git a/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala b/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala index 5973972..f6c1bf2 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala +++ b/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala @@ -1,6 +1,7 @@ package io.oath.test import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.* import com.auth0.jwt.{JWT, JWTCreator} import io.oath.* import io.oath.config.JwtVerifierConfig.* @@ -23,7 +24,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper LeewayWindowConfig(None, None, None, None), ) - def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig): TestData = + def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig): TestData = { val now = getInstantNowSeconds val leeway = config.leewayWindow.leeway.map(leeway => now.plusSeconds(leeway.toSeconds - 1)) val expiresAt = config.leewayWindow.expiresAt.map(expiresAt => now.plusSeconds(expiresAt.toSeconds - 1)) @@ -49,9 +50,10 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) TestData(registeredClaims, builderWithRegistered) + } - "JwtVerifier" should: - "verify token with prerequisite configurations" in forAll: (config: JwtVerifierConfig) => + "JwtVerifier" should { + "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) val testData = setRegisteredClaims(JWT.create(), config) @@ -61,8 +63,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken).value verified.registered shouldBe testData.registeredClaims + } - "verify a token with header" in forAll: (nestedHeader: NestedHeader, config: JwtVerifierConfig) => + "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) val testData = setRegisteredClaims(JWT.create(), config) @@ -74,8 +77,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) + } - "verify a token with header that is encrypted" in forAll: + "verify a token with header that is encrypted" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig, encryptConfig: EncryptConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = Some(encryptConfig))) @@ -90,8 +94,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) + } - "verify a token with payload" in forAll: (nestedPayload: NestedPayload, config: JwtVerifierConfig) => + "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) val testData = setRegisteredClaims(JWT.create(), config) @@ -103,8 +108,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) + } - "verify a token with payload that is encrypted" in forAll: + "verify a token with payload that is encrypted" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig, encryptConfig: EncryptConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = Some(encryptConfig))) @@ -119,8 +125,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) + } - "verify a token with header & payload" in forAll: + "verify a token with header & payload" in forAll { (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) @@ -135,8 +142,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) + } - "verify a token with header & payload that is encrypted" in forAll: + "verify a token with header & payload that is encrypted" in forAll { ( nestedHeader: NestedHeader, nestedPayload: NestedPayload, @@ -158,8 +166,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) + } - "fail to verify a token that is encrypted" in: + "fail to verify a token that is encrypted" in { val encryptConfig = defaultConfig.copy(encrypt = Some(EncryptConfig("secret"))) val jwtVerifier = new JwtVerifier(encryptConfig) @@ -180,8 +189,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper failedOutOfRangeLonger.left.value shouldBe a[JwtVerifyError.DecryptionError] failedOutOfRangeShorter.left.value shouldBe a[JwtVerifyError.DecryptionError] failedNotValid.left.value shouldBe a[JwtVerifyError.DecryptionError] + } - "fail to decode a token with header" in: + "fail to decode a token with header" in { val jwtVerifier = new JwtVerifier(defaultConfig) val header = """{"name": "name"}""" @@ -193,8 +203,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } - "fail to decode a token with payload" in: + "fail to decode a token with payload" in { val jwtVerifier = new JwtVerifier(defaultConfig) val payload = """{"name": "name"}""" @@ -206,8 +217,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } - "fail to decode a token with header & payload" in: + "fail to decode a token with header & payload" in { val jwtVerifier = new JwtVerifier(defaultConfig) val header = """{"name": "name"}""" @@ -220,8 +232,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } - "fail to decode a token with header if exception raised in decoder" in: + "fail to decode a token with header if exception raised in decoder" in { val jwtVerifier = new JwtVerifier(defaultConfig) val token = JWT @@ -230,9 +243,10 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimpleHeader](token.toTokenH) - verified.left.value.error shouldBe "Boom" + verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + } - "fail to decode a token with payload if exception raised in decoder" in: + "fail to decode a token with payload if exception raised in decoder" in { val jwtVerifier = new JwtVerifier(defaultConfig) val token = JWT @@ -241,9 +255,10 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimplePayload](token.toTokenP) - verified.left.value.error shouldBe "Boom" + verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + } - "fail to decode a token with header & payload if exception raised in decoder" in: + "fail to decode a token with header & payload if exception raised in decoder" in { val jwtVerifier = new JwtVerifier(defaultConfig) val token = JWT @@ -253,9 +268,10 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) - verified.left.value.error shouldBe "Boom" + verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + } - "fail to verify token with VerificationError when provided with claims are not meet criteria" in: + "fail to verify token with VerificationError when provided with claims are not meet criteria" in { val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) val jwtVerifier = new JwtVerifier(config) @@ -265,9 +281,13 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldBe JwtVerifyError.VerificationError("The Claim 'iss' is not present in the JWT.") + verified.left.value shouldEqual JwtVerifyError.VerificationError( + "JwtVerifier failed with JWTVerificationException", + Some(new JWTVerificationException("The Claim 'iss' is not present in the JWT.")), + ) + } - "fail to verify token with IllegalArgument when null algorithm is provided" in forAll: + "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { (config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(algorithm = null, encrypt = None)) @@ -277,9 +297,13 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldBe JwtVerifyError.IllegalArgument("The Algorithm cannot be null.") + verified.left.value shouldEqual JwtVerifyError.IllegalArgument( + "JwtVerifier failed with IllegalArgumentException", + new IllegalArgumentException("The Algorithm cannot be null."), + ) + } - "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll: + "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { (config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"), encrypt = None)) @@ -289,27 +313,33 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldBe + verified.left.value shouldEqual JwtVerifyError - .AlgorithmMismatch("The provided Algorithm doesn't match the one defined in the JWT's Header.") + .AlgorithmMismatch( + "JwtVerifier failed with AlgorithmMismatchException", + new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected."), + ) + } - "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll: + "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { (config: JwtVerifierConfig) => val jwtVerifier = new JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"), encrypt = None)) - + val algorithm = Algorithm.HMAC256("secret1") val token = JWT .create() - .sign(Algorithm.HMAC256("secret1")) + .sign(algorithm) val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldBe + verified.left.value shouldEqual JwtVerifyError .SignatureVerificationError( - "The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256" + "JwtVerifier failed with SignatureVerificationException", + new SignatureVerificationException(algorithm), ) + } - "fail to verify token with TokenExpired when JWT expires" in: + "fail to verify token with TokenExpired when JWT expires" in { val jwtVerifier = new JwtVerifier(defaultConfig) val expiresAt = getInstantNowSeconds.minusSeconds(1) @@ -323,8 +353,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.left.value shouldBe JwtVerifyError .TokenExpired(s"The Token has expired on $expiresAt.") + } - "fail to verify an empty string token" in: + "fail to verify an empty string token" in { val jwtVerifier = new JwtVerifier(defaultConfig) val token = "" val verified = jwtVerifier.verifyJwt(token.toToken) @@ -347,3 +378,5 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verifiedHP.left.value shouldBe JwtVerifyError .VerificationError("JWT Token is empty.") + } + } diff --git a/modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala b/modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala new file mode 100644 index 0000000..d541282 --- /dev/null +++ b/modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala @@ -0,0 +1,8 @@ +package io.oath.jsoniter_scala + +import com.github.plokhotnyuk.jsoniter_scala.core.* +import io.oath.json.* +import io.oath.jsoniter_scala.syntax.* + +object conversion: + given [P](using codec: JsonValueCodec[P]): ClaimsCodec[P] = codec.convert diff --git a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala b/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala index b8bf052..a5b71aa 100644 --- a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala +++ b/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala @@ -7,7 +7,5 @@ import io.oath.jsoniter_scala.syntax.* final case class Bar(name: String, age: Int) -object Bar { - +object Bar: implicit val codecBar: ClaimsCodec[Bar] = JsonCodecMaker.make.convert -} diff --git a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala index dc42157..8c3923d 100644 --- a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala +++ b/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala @@ -6,6 +6,7 @@ import io.oath.* import io.oath.config.JwtIssuerConfig.RegisteredConfig import io.oath.config.JwtVerifierConfig.* import io.oath.config.{JwtIssuerConfig, JwtVerifierConfig} +import io.oath.json.ClaimsDecoder import io.oath.syntax.* import io.oath.testkit.AnyWordSpecBase import io.oath.utils.CodecUtils @@ -29,16 +30,17 @@ class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils: val jwtVerifier = new JwtVerifier(verifierConfig) val jwtIssuer = new JwtIssuer(issuerConfig) - "JsoniterConversion" should: + "JsoniterConversion" should { - "convert jsoniter codec to claims (encoders & decoders)" in: + "convert jsoniter codec to claims (encoders & decoders)" in { val bar = Bar("bar", 10) val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(bar)).value val claims = jwtVerifier.verifyJwt[Bar](jwt.token.toTokenP).value claims.payload shouldBe bar + } - "convert jsoniter codec to claims decoder and get error" in: + "convert jsoniter codec to claims decoder and get error" in { val fooJson = """{"name":"Hello","age":"not number"}""" val jwt = JWT .create() @@ -46,5 +48,12 @@ class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils: .sign(Algorithm.none()) val claims = jwtVerifier.verifyJwt[Bar](jwt.toTokenP) - val decodingError: JwtVerifyError = claims.left.value - decodingError.error should startWith("illegal number, offset: 0x00000016, buf:") + claims.left.value shouldBe a[JwtVerifyError.DecodingError] + } + + "convert jsoniter decoder to claims decoder and get error when format is incorrect" in { + val barJson = """{"name":,}""" + + summon[ClaimsDecoder[Bar]].decode(barJson).left.value shouldBe a[JwtVerifyError.DecodingError] + } + } diff --git a/project/Projects.scala b/project/Projects.scala index 14180b2..fb5ad28 100644 --- a/project/Projects.scala +++ b/project/Projects.scala @@ -1,21 +1,10 @@ -import sbt.Keys.* -import sbt.* import org.scalafmt.sbt.ScalafmtPlugin.autoImport.scalafmtOnCompile +import sbt.* +import sbt.Keys.* import scalafix.sbt.ScalafixPlugin.autoImport.scalafixOnCompile object Projects { - def createModule(moduleName: String): Project = createModule(moduleName, moduleName) - def createModule(moduleName: String, fileName: String): Project = - Project(moduleName, base = file(fileName)) - .settings( - Test / fork := true, - run / fork := true, - Test / parallelExecution := false, - scalafmtOnCompile := sys.env.getOrElse("RUN_SCALAFMT_ON_COMPILE", "false").toBoolean, - scalafixOnCompile := sys.env.getOrElse("RUN_SCALAFIX_ON_COMPILE", "false").toBoolean, - semanticdbEnabled := true, - semanticdbVersion := "4.8.15", - ) + } From 386efb3b61e440e96adbe9785fc6ba1cdba8befe Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 28 Jul 2024 04:03:48 +0100 Subject: [PATCH 02/15] feat: update --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2956bc..919c32a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,11 +95,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) - run: mkdir -p modules/oath-circe/target modules/oath-jsoniter-scala/target modules/oath-core/target modules/oath-macros/target project/target + run: mkdir -p oath/circe/target oath/target oath/jsoniter-scala/target example/target oath/core/target oath/macros/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) - run: tar cf targets.tar modules/oath-circe/target modules/oath-jsoniter-scala/target modules/oath-core/target modules/oath-macros/target project/target + run: tar cf targets.tar oath/circe/target oath/target oath/jsoniter-scala/target example/target oath/core/target oath/macros/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) From f8dc4fde8cf976f79e799fa1c1b8feabe6b341b2 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 28 Jul 2024 04:04:59 +0100 Subject: [PATCH 03/15] feat: remove plugin --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index b5ea877..3e1cf9a 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,6 @@ ThisBuild / scalaVersion := "3.3.3" ThisBuild / organization := "io.github.scala-jwt" ThisBuild / organizationName := "oath" ThisBuild / organizationHomepage := Some(url("https://github.com/scala-jwt/oath")) -ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0" ThisBuild / tlBaseVersion := "2.0" ThisBuild / tlMimaPreviousVersions := Set.empty ThisBuild / licenses := Seq(License.Apache2) From ac479c533435bbd205e4e67fc82c10cfd1df0dea Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 28 Jul 2024 20:49:37 +0100 Subject: [PATCH 04/15] feat: improve codebase --- build.sbt | 63 +++++++++++--- .../io/oath/circe/CirceConversionSpec.scala | 72 --------------- .../main/scala/io/oath/config/config.scala | 57 ------------ .../src/main/scala/io/oath/package.scala | 14 --- .../main/scala/io/oath/syntax/package.scala | 16 ---- .../main/scala/io/oath/utils/package.scala | 74 ---------------- .../main/scala/io/oath/circe/conversion.scala | 3 +- .../src/main/scala/io/oath/circe/syntax.scala | 9 +- .../src/test/scala/io/oath/circe/Bar.scala | 5 +- .../io/oath/circe/CirceConversionSpec.scala | 70 +++++++++++++++ .../src/test/scala/io/oath/circe/Foo.scala | 3 +- .../scala/io/oath/test}/AnyWordSpecBase.scala | 2 +- .../scala/io/oath/test}/Arbitraries.scala | 37 ++++---- .../scala/io/oath/test}/ClockHelper.scala | 5 +- .../main/scala/io/oath/test}/CodecUtils.scala | 5 +- .../scala/io/oath/test/NestedHeader.scala | 11 ++- .../scala/io/oath/test/NestedPayload.scala | 4 +- .../main}/scala/io/oath/test/OathToken.scala | 3 +- .../io/oath/test}/PropertyBasedTesting.scala | 5 +- .../main/scala/io/oath/test}/TestData.scala | 2 +- .../src/test/resources/algorithm-es256.conf | 0 .../src/test/resources/algorithm-es384.conf | 0 .../src/test/resources/algorithm-es512.conf | 0 .../src/test/resources/algorithm-hsxxx.conf | 0 .../src/test/resources/algorithm-none.conf | 0 .../src/test/resources/algorithm-rsxxx.conf | 0 .../test/resources/algorithm-unsupported.conf | 0 .../core-test}/src/test/resources/issuer.conf | 0 .../src/test/resources/manager.conf | 0 .../src/test/resources/reference.conf | 0 .../src/test/resources/verifier.conf | 0 .../test/scala/io/oath}/JwtIssuerSpec.scala | 10 +-- .../test/scala/io/oath}/JwtManagerSpec.scala | 25 +++--- .../test/scala/io/oath}/JwtVerifierSpec.scala | 12 +-- .../test/scala/io/oath}/OathIssuerSpec.scala | 14 +-- .../test/scala/io/oath}/OathManagerSpec.scala | 16 ++-- .../scala/io/oath}/OathVerifierSpec.scala | 17 ++-- .../io/oath}/config/AlgorithmLoaderSpec.scala | 34 +++++--- .../io/oath}/config/JwtIssuerLoaderSpec.scala | 46 ++++++---- .../oath}/config/JwtManagerLoaderSpec.scala | 14 +-- .../oath}/config/JwtVerifierLoaderSpec.scala | 32 ++++--- .../test/scala/io/oath}/utils/UtilsSpec.scala | 16 ++-- .../src/test/secrets/es256-private.pem | 0 .../src/test/secrets/es256-public.pem | 0 .../src/test/secrets/es384-private.pem | 0 .../src/test/secrets/es384-public.pem | 0 .../src/test/secrets/es512-private.pem | 0 .../src/test/secrets/es512-public.pem | 0 .../src/test/secrets/rsa-private.pem | 0 .../src/test/secrets/rsa-public.pem | 0 .../core}/src/main/scala/io/oath/Jwt.scala | 0 .../src/main/scala/io/oath/JwtClaims.scala | 6 +- .../main/scala/io/oath/JwtIssueError.scala | 19 ++-- .../src/main/scala/io/oath/JwtIssuer.scala | 5 +- .../src/main/scala/io/oath/JwtManager.scala | 0 .../src/main/scala/io/oath/JwtToken.scala | 6 +- .../src/main/scala/io/oath/JwtVerifier.scala | 5 +- .../main/scala/io/oath/JwtVerifyError.scala | 29 +++++-- .../src/main/scala/io/oath/OathIssuer.scala | 6 +- .../src/main/scala/io/oath/OathManager.scala | 6 +- .../src/main/scala/io/oath/OathVerifier.scala | 3 +- .../main/scala/io/oath/RegisteredClaims.scala | 5 +- .../io/oath/config/AlgorithmLoader.scala | 16 ++-- .../scala/io/oath/config/EncryptConfig.scala | 3 +- .../io/oath/config/JwtIssuerConfig.scala | 18 ++-- .../io/oath/config/JwtManagerConfig.scala | 5 +- .../io/oath/config/JwtVerifierConfig.scala | 25 +++--- .../main/scala/io/oath/config/package.scala | 55 ++++++++++++ .../main/scala/io/oath/json/ClaimsCodec.scala | 0 .../scala/io/oath/json/ClaimsDecoder.scala | 0 .../scala/io/oath/json/ClaimsEncoder.scala | 0 .../core/src/main/scala/io/oath/package.scala | 14 +++ .../scala/io/oath/syntax/JwtClaimsOps.scala | 14 +++ .../scala/io/oath/syntax/JwtTokenOps.scala | 14 +++ .../io/oath/syntax/RegisteredClaimsOps.scala | 9 ++ .../src/main/scala/io/oath/syntax/all.scala | 3 + .../scala/io/oath/utils/DecryptionUtils.scala | 6 +- .../scala/io/oath/utils/EncryptionUtils.scala | 3 +- .../main/scala/io/oath/utils/package.scala | 64 ++++++++++++++ .../io/oath/jsoniter_scala/conversion.scala | 0 .../scala/io/oath/jsoniter_scala/syntax.scala | 0 .../scala/io/oath/jsoniter_scala/Bar.scala | 0 .../JsoniterConversionSpec.scala | 0 .../main/scala/io/oath/OathEnumMacro.scala | 0 .../src/test/scala/io/oath/OathEnum.scala | 0 .../scala/io/oath/OathEnumMacroSpec.scala | 0 project/Dependencies.scala | 87 ++++++------------- project/Projects.scala | 6 +- 88 files changed, 622 insertions(+), 506 deletions(-) delete mode 100644 modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala delete mode 100644 modules/oath-core/src/main/scala/io/oath/config/config.scala delete mode 100644 modules/oath-core/src/main/scala/io/oath/package.scala delete mode 100644 modules/oath-core/src/main/scala/io/oath/syntax/package.scala delete mode 100644 modules/oath-core/src/main/scala/io/oath/utils/package.scala rename {modules/oath-circe => oath/circe}/src/main/scala/io/oath/circe/conversion.scala (93%) rename {modules/oath-circe => oath/circe}/src/main/scala/io/oath/circe/syntax.scala (78%) rename {modules/oath-circe => oath/circe}/src/test/scala/io/oath/circe/Bar.scala (77%) create mode 100644 oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala rename {modules/oath-circe => oath/circe}/src/test/scala/io/oath/circe/Foo.scala (92%) rename {modules/oath-core/src/test/scala/io/oath/testkit => oath/core-test/src/main/scala/io/oath/test}/AnyWordSpecBase.scala (90%) rename {modules/oath-core/src/test/scala/io/oath/testkit => oath/core-test/src/main/scala/io/oath/test}/Arbitraries.scala (94%) rename {modules/oath-core/src/test/scala/io/oath/utils => oath/core-test/src/main/scala/io/oath/test}/ClockHelper.scala (85%) rename {modules/oath-core/src/test/scala/io/oath/utils => oath/core-test/src/main/scala/io/oath/test}/CodecUtils.scala (84%) rename {modules/oath-core/src/test => oath/core-test/src/main}/scala/io/oath/test/NestedHeader.scala (87%) rename {modules/oath-core/src/test => oath/core-test/src/main}/scala/io/oath/test/NestedPayload.scala (90%) rename {modules/oath-core/src/test => oath/core-test/src/main}/scala/io/oath/test/OathToken.scala (83%) rename {modules/oath-core/src/test/scala/io/oath/testkit => oath/core-test/src/main/scala/io/oath/test}/PropertyBasedTesting.scala (90%) rename {modules/oath-core/src/test/scala/io/oath/utils => oath/core-test/src/main/scala/io/oath/test}/TestData.scala (87%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-es256.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-es384.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-es512.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-hsxxx.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-none.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-rsxxx.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/algorithm-unsupported.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/issuer.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/manager.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/reference.conf (100%) rename {modules/oath-core => oath/core-test}/src/test/resources/verifier.conf (100%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/JwtIssuerSpec.scala (99%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/JwtManagerSpec.scala (88%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/JwtVerifierSpec.scala (98%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/OathIssuerSpec.scala (84%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/OathManagerSpec.scala (85%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/OathVerifierSpec.scala (89%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/config/AlgorithmLoaderSpec.scala (84%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/config/JwtIssuerLoaderSpec.scala (81%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/config/JwtManagerLoaderSpec.scala (88%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/config/JwtVerifierLoaderSpec.scala (92%) rename {modules/oath-core/src/test/scala/io/oath/test => oath/core-test/src/test/scala/io/oath}/utils/UtilsSpec.scala (69%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es256-private.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es256-public.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es384-private.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es384-public.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es512-private.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/es512-public.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/rsa-private.pem (100%) rename {modules/oath-core => oath/core-test}/src/test/secrets/rsa-public.pem (100%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/Jwt.scala (100%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtClaims.scala (91%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtIssueError.scala (52%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtIssuer.scala (98%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtManager.scala (100%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtToken.scala (92%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtVerifier.scala (98%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/JwtVerifyError.scala (57%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/OathIssuer.scala (88%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/OathManager.scala (87%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/OathVerifier.scala (96%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/RegisteredClaims.scala (84%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/config/AlgorithmLoader.scala (92%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/config/EncryptConfig.scala (95%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/config/JwtIssuerConfig.scala (86%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/config/JwtManagerConfig.scala (84%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/config/JwtVerifierConfig.scala (86%) create mode 100644 oath/core/src/main/scala/io/oath/config/package.scala rename {modules/oath-core => oath/core}/src/main/scala/io/oath/json/ClaimsCodec.scala (100%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/json/ClaimsDecoder.scala (100%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/json/ClaimsEncoder.scala (100%) create mode 100644 oath/core/src/main/scala/io/oath/package.scala create mode 100644 oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala create mode 100644 oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala create mode 100644 oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala create mode 100644 oath/core/src/main/scala/io/oath/syntax/all.scala rename {modules/oath-core => oath/core}/src/main/scala/io/oath/utils/DecryptionUtils.scala (90%) rename {modules/oath-core => oath/core}/src/main/scala/io/oath/utils/EncryptionUtils.scala (96%) create mode 100644 oath/core/src/main/scala/io/oath/utils/package.scala rename {modules/oath-jsoniter-scala => oath/jsoniter-scala}/src/main/scala/io/oath/jsoniter_scala/conversion.scala (100%) rename {modules/oath-jsoniter-scala => oath/jsoniter-scala}/src/main/scala/io/oath/jsoniter_scala/syntax.scala (100%) rename {modules/oath-jsoniter-scala => oath/jsoniter-scala}/src/test/scala/io/oath/jsoniter_scala/Bar.scala (100%) rename {modules/oath-jsoniter-scala => oath/jsoniter-scala}/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala (100%) rename {modules/oath-macros => oath/macros}/src/main/scala/io/oath/OathEnumMacro.scala (100%) rename {modules/oath-macros => oath/macros}/src/test/scala/io/oath/OathEnum.scala (100%) rename {modules/oath-macros => oath/macros}/src/test/scala/io/oath/OathEnumMacroSpec.scala (100%) diff --git a/build.sbt b/build.sbt index 3e1cf9a..b808ab1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,11 @@ +import Dependencies.* import org.typelevel.sbt.gha.Permissions +import scala.util.chaining.* + Global / onChangedBuildSource := ReloadOnSourceChanges -ThisBuild / scalaVersion := "3.3.3" +ThisBuild / scalaVersion := "3.4.1" ThisBuild / organization := "io.github.scala-jwt" ThisBuild / organizationName := "oath" ThisBuild / organizationHomepage := Some(url("https://github.com/scala-jwt/oath")) @@ -51,14 +54,20 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( ThisBuild / Test / fork := true ThisBuild / run / fork := true -ThisBuild / Test / parallelExecution := false +ThisBuild / Test / parallelExecution := true ThisBuild / scalafmtOnCompile := sys.env.getOrElse("RUN_SCALAFMT_ON_COMPILE", "false").toBoolean ThisBuild / scalafixOnCompile := sys.env.getOrElse("RUN_SCALAFIX_ON_COMPILE", "false").toBoolean ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := "4.8.15" +lazy val rootModuleName = "root" + def rootModule(rootModule: String)(subModule: String): Project = - Project(s"$rootModule-$subModule", file(s"$rootModule${if (subModule == "root") "" else s"/$subModule"}")) + Project(s"$rootModule-$subModule", file(s"$rootModule${if (subModule == rootModuleName) "" else s"/$subModule"}")) + .pipe(project => + if (subModule == rootModuleName) project.enablePlugins(NoPublishPlugin) + else project + ) lazy val root = Project("oath", file(".")) .enablePlugins(NoPublishPlugin) @@ -67,7 +76,7 @@ lazy val root = Project("oath", file(".")) lazy val example = project .in(file("example")) - .dependsOn(oathCore) + .dependsOn(oathCore, oathCirce, oathJsoniterScala) val createOathModule = rootModule("oath") _ @@ -75,23 +84,57 @@ lazy val oathRoot = createOathModule("root") .aggregate(oathModules *) lazy val oathMacros = createOathModule("macros") - .settings(Dependencies.oathMacros) + .settings( + libraryDependencies ++= Seq( + scalaTest % Test, + scalaTestPlusScalaCheck % Test, + scalacheck % Test, + ) + ) lazy val oathCore = createOathModule("core") .dependsOn(oathMacros) - .settings(Dependencies.oathCore) + .settings( + libraryDependencies ++= Seq( + javaJWT, + typesafeConfig, + bcprov, + cats, + ) + ) + +lazy val oathCoreTest = createOathModule("core-test") + .enablePlugins(NoPublishPlugin) + .dependsOn(oathCore) + .settings( + libraryDependencies ++= Seq( + scalaTest, + scalaTestPlusScalaCheck, + scalacheck, + circeCore, + circeGeneric, + circeParser, + ) + ) lazy val oathCirce = createOathModule("circe") - .settings(Dependencies.oathCirce) - .dependsOn(oathCore % "compile->compile;test->test") + .dependsOn( + oathCore, + oathCoreTest % Test, + ) + .settings(libraryDependencies ++= Seq(circeCore, circeGeneric, circeParser)) lazy val oathJsoniterScala = createOathModule("jsoniter-scala") - .settings(Dependencies.oathJsoniterScala) - .dependsOn(oathCore % "compile->compile;test->test") + .dependsOn( + oathCore, + oathCoreTest % Test, + ) + .settings(libraryDependencies ++= Seq(jsoniterScalacore, jsoniterScalamacros)) lazy val oathModules: Seq[ProjectReference] = Seq( oathMacros, oathCore, + oathCoreTest, oathCirce, oathJsoniterScala, ) diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala deleted file mode 100644 index 40b5dd7..0000000 --- a/modules/oath-circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala +++ /dev/null @@ -1,72 +0,0 @@ -package io.oath.circe - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import io.oath.* -import io.oath.circe.conversion.given -import io.oath.config.JwtIssuerConfig.RegisteredConfig -import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} -import io.oath.config.* -import io.oath.json.ClaimsDecoder -import io.oath.syntax.* -import io.oath.testkit.AnyWordSpecBase -import io.oath.utils.CodecUtils -import org.typelevel.jawn.ParseException - -class CirceConversionSpec extends AnyWordSpecBase, CodecUtils: - - val verifierConfig = - JwtVerifierConfig( - Algorithm.HMAC256("secret"), - None, - ProvidedWithConfig(None, None, Nil), - LeewayWindowConfig(None, None, None, None), - ) - - val issuerConfig = - JwtIssuerConfig( - Algorithm.HMAC256("secret"), - None, - RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), - ) - - val jwtVerifier = new JwtVerifier(verifierConfig) - val jwtIssuer = new JwtIssuer(issuerConfig) - - "CirceConversion" should { - "convert circe (encoders & decoders) to claims (encoders & decoders)" in { - val bar = Bar("bar", 10) - val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(bar)).value - val claims = jwtVerifier.verifyJwt[Bar](jwt.token.toTokenP).value - - claims.payload shouldBe bar - } - - "convert circe (codec) to claims (encoders & decoders)" in { - val foo = Foo("foo", 10) - val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("issuer")))).value - val claims = jwtVerifier.verifyJwt[Foo](jwt.token.toTokenP).value - - claims.payload shouldBe foo - } - - "convert circe decoder to claims decoder and get error" in { - val fooJson = """{"name":"Hello","age":"not number"}""" - val jwt = JWT - .create() - .withPayload(unsafeParseJsonToJavaMap(fooJson)) - .sign(Algorithm.HMAC256("secret")) - val claims = jwtVerifier.verifyJwt[Foo](jwt.toTokenP) - - claims.left.value shouldBe JwtVerifyError.DecodingError("DecodingFailure at .age: Int", null) - } - - "convert circe decoder to claims decoder and get error when format is incorrect" in { - val fooJson = """{"name":,}""" - - summon[ClaimsDecoder[Foo]].decode(fooJson).left.value shouldEqual JwtVerifyError.DecodingError( - "expected json value got ',}' (line 1, column 9)", - ParseException("expected json value got ',}' (line 1, column 9)", 8, 1, 9), - ) - } - } diff --git a/modules/oath-core/src/main/scala/io/oath/config/config.scala b/modules/oath-core/src/main/scala/io/oath/config/config.scala deleted file mode 100644 index 427fa06..0000000 --- a/modules/oath-core/src/main/scala/io/oath/config/config.scala +++ /dev/null @@ -1,57 +0,0 @@ -package io.oath - -import com.typesafe.config.{Config, ConfigException, ConfigFactory} - -import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters.* -import scala.jdk.DurationConverters.* -import scala.util.chaining.* -import scala.util.control.Exception.allCatch - -package config: - inline private[config] val OathLocation = "oath" - private[config] val rootConfig = ConfigFactory.load().getConfig(OathLocation) - - extension (config: Config) - private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { case _: ConfigException.Missing => - default - } - - private[config] def getMaybeNonEmptyString(path: String): Option[String] = - allCatch - .withTry(Some(config.getString(path))) - .recover(ifMissingDefault(Option.empty)) - .toOption - .flatten - .tap(value => - if (value.exists(_.isEmpty)) throw new IllegalArgumentException(s"$path empty string not allowed.") - ) - - private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = - allCatch - .withTry(Some(config.getDuration(path).toScala)) - .recover(ifMissingDefault(None)) - .get - - private[config] def getBooleanDefaultFalse(path: String): Boolean = - allCatch - .withTry(config.getBoolean(path)) - .recover(ifMissingDefault(false)) - .get - - private[config] def getSeqNonEmptyString(path: String): Seq[String] = - allCatch - .withTry(config.getStringList(path).asScala.toSeq) - .recover(ifMissingDefault(Seq.empty)) - .get - .tap(value => - if value.exists(_.isEmpty) then - throw new IllegalArgumentException(s"$path empty string in the list not allowed.") - ) - - private[config] def getMaybeConfig(path: String): Option[Config] = - allCatch - .withTry(Some(config.getConfig(path))) - .recover(ifMissingDefault(Option.empty)) - .toOption - .flatten diff --git a/modules/oath-core/src/main/scala/io/oath/package.scala b/modules/oath-core/src/main/scala/io/oath/package.scala deleted file mode 100644 index 99d3adf..0000000 --- a/modules/oath-core/src/main/scala/io/oath/package.scala +++ /dev/null @@ -1,14 +0,0 @@ -package io -import io.oath.utils.* - -// Type aliases with extra information, useful to determine the token type. -package oath: - type JIssuer[_] = JwtIssuer - type JManager[_] = JwtManager - type JVerifier[_] = JwtVerifier - - inline private def getEnumValues[A]: Set[(A, String)] = - OathEnumMacro - .enumValues[A] - .toSet - .map(value => value -> convertUpperCamelToLowerHyphen(value.toString)) diff --git a/modules/oath-core/src/main/scala/io/oath/syntax/package.scala b/modules/oath-core/src/main/scala/io/oath/syntax/package.scala deleted file mode 100644 index 8aed960..0000000 --- a/modules/oath-core/src/main/scala/io/oath/syntax/package.scala +++ /dev/null @@ -1,16 +0,0 @@ -package io.oath - -package syntax: - extension (value: String) - def toToken: JwtToken.Token = JwtToken.Token(value) - def toTokenH: JwtToken.TokenH = JwtToken.TokenH(value) - def toTokenP: JwtToken.TokenP = JwtToken.TokenP(value) - def toTokenHP: JwtToken.TokenHP = JwtToken.TokenHP(value) - - extension (value: RegisteredClaims) def toClaims: JwtClaims.Claims = JwtClaims.Claims(value) - - extension [A](value: A) - def toClaimsP: JwtClaims.ClaimsP[A] = JwtClaims.ClaimsP(value) - def toClaimsH: JwtClaims.ClaimsH[A] = JwtClaims.ClaimsH(value) - - extension [A, B](value: (A, B)) def toClaimsHP: JwtClaims.ClaimsHP[A, B] = JwtClaims.ClaimsHP(value._1, value._2) diff --git a/modules/oath-core/src/main/scala/io/oath/utils/package.scala b/modules/oath-core/src/main/scala/io/oath/utils/package.scala deleted file mode 100644 index cd6c162..0000000 --- a/modules/oath-core/src/main/scala/io/oath/utils/package.scala +++ /dev/null @@ -1,74 +0,0 @@ -package io.oath - -import com.auth0.jwt.JWTCreator.Builder -import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.json.ClaimsEncoder - -import java.nio.charset.StandardCharsets -import java.time.Instant -import java.util.Base64 -import scala.jdk.CollectionConverters.CollectionHasAsScala -import scala.util.chaining.scalaUtilChainingOps -import scala.util.control.Exception.allCatch - -package utils: - inline private[utils] val AES: "AES" = "AES" - inline private[utils] val UTF8: "utf-8" = "utf-8" - - private[oath] def convertUpperCamelToLowerHyphen(str: String): String = - str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim - - private[oath] def base64DecodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = - allCatch - .withTry(new String(Base64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) - .toEither - .left - .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) - - extension (decodedJWT: DecodedJWT) - private[oath] def getOptionIssuer: Option[String] = - Option(decodedJWT.getIssuer) - - private[oath] def getOptionSubject: Option[String] = - Option(decodedJWT.getSubject) - - private[oath] def getOptionJwtID: Option[String] = - Option(decodedJWT.getId) - - private[oath] def getSeqAudience: Seq[String] = - Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten - - private[oath] def getOptionExpiresAt: Option[Instant] = - Option(decodedJWT.getExpiresAt).map(_.toInstant) - - private[oath] def getOptionIssueAt: Option[Instant] = - Option(decodedJWT.getIssuedAt).map(_.toInstant) - - private[oath] def getOptionNotBefore: Option[Instant] = - Option(decodedJWT.getNotBefore).map(_.toInstant) - - extension (builder: Builder) - - private def safeEncode[T]( - claims: T, - toBuilder: String => Builder, - )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = - allCatch - .withTry( - claimsEncoder - .encode(claims) - .pipe(toBuilder) - ) - .toEither - .left - .map(error => JwtIssueError.EncodeError(error.getMessage)) - - private[oath] def safeEncodeHeader[H](claims: H)(using - ClaimsEncoder[H] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withHeader) - - private[oath] def safeEncodePayload[P](claims: P)(using - ClaimsEncoder[P] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withPayload) diff --git a/modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala b/oath/circe/src/main/scala/io/oath/circe/conversion.scala similarity index 93% rename from modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala rename to oath/circe/src/main/scala/io/oath/circe/conversion.scala index d1b45d0..7a033ba 100644 --- a/modules/oath-circe/src/main/scala/io/oath/circe/conversion.scala +++ b/oath/circe/src/main/scala/io/oath/circe/conversion.scala @@ -4,9 +4,10 @@ import io.circe.* import io.oath.circe.syntax.* import io.oath.json.* -object conversion: +object conversion { given [P](using encoder: Encoder[P]): ClaimsEncoder[P] = encoder.convert given [P](using decoder: Decoder[P]): ClaimsDecoder[P] = decoder.convert given [P](using codec: Codec[P]): ClaimsCodec[P] = codec.convertCodec +} diff --git a/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala b/oath/circe/src/main/scala/io/oath/circe/syntax.scala similarity index 78% rename from modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala rename to oath/circe/src/main/scala/io/oath/circe/syntax.scala index 3102a2f..6bf1eeb 100644 --- a/modules/oath-circe/src/main/scala/io/oath/circe/syntax.scala +++ b/oath/circe/src/main/scala/io/oath/circe/syntax.scala @@ -5,11 +5,11 @@ import io.circe.syntax.EncoderOps import io.oath.JwtVerifyError import io.oath.json.{ClaimsCodec, ClaimsDecoder, ClaimsEncoder} -object syntax: - extension [P](encoder: Encoder[P]) def convert: ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces +object syntax { + extension [P](encoder: Encoder[P]) inline def convert: ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces extension [P](decoder: Decoder[P]) - def convert: ClaimsDecoder[P] = + inline def convert: ClaimsDecoder[P] = json => parser .parse(json) @@ -22,9 +22,10 @@ object syntax: ) extension [P](codec: Codec[P]) - def convertCodec: ClaimsCodec[P] = new ClaimsCodec[P]: + inline def convertCodec: ClaimsCodec[P] = new ClaimsCodec[P]: override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = codec.asInstanceOf[Decoder[P]].convert.decode(token) override def encode(data: P): String = codec.asInstanceOf[Encoder[P]].convert.encode(data) +} diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala b/oath/circe/src/test/scala/io/oath/circe/Bar.scala similarity index 77% rename from modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala rename to oath/circe/src/test/scala/io/oath/circe/Bar.scala index 6b3c00c..e1ead9c 100644 --- a/modules/oath-circe/src/test/scala/io/oath/circe/Bar.scala +++ b/oath/circe/src/test/scala/io/oath/circe/Bar.scala @@ -3,8 +3,9 @@ package io.oath.circe import io.circe.generic.semiauto.* import io.circe.{Decoder, Encoder} -case class Bar(name: String, age: Int) +final case class Bar(name: String, age: Int) -object Bar: +object Bar { given barEncoder: Encoder[Bar] = deriveEncoder[Bar] given barDecoder: Decoder[Bar] = deriveDecoder[Bar] +} diff --git a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala new file mode 100644 index 0000000..a92f222 --- /dev/null +++ b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala @@ -0,0 +1,70 @@ +package io.oath.circe + +import com.auth0.jwt.algorithms.Algorithm +import io.oath.* +import io.oath.circe.conversion.given +import io.oath.config.JwtIssuerConfig.RegisteredConfig +import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} +import io.oath.config.* +import io.oath.syntax.* +import io.oath.testkit.AnyWordSpecBase +import io.oath.utils.CodecUtils + +class CirceConversionSpec extends AnyWordSpecBase, CodecUtils { + + val verifierConfig = + JwtVerifierConfig( + Algorithm.HMAC256("secret"), + None, + ProvidedWithConfig(None, None, Nil), + LeewayWindowConfig(None, None, None, None), + ) + + val issuerConfig = + JwtIssuerConfig( + Algorithm.HMAC256("secret"), + None, + RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), + ) + + val jwtVerifier = new JwtVerifier(verifierConfig) + val jwtIssuer = new JwtIssuer(issuerConfig) + + "CirceConversion" should { + "convert circe (encoders & decoders) to claims (encoders & decoders)" in { + val bar = Bar("bar", 10) + val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(bar)).value + val claims = jwtVerifier.verifyJwt[Bar](jwt.token.toTokenP).value + + claims.payload shouldBe bar + } + // + // "convert circe (codec) to claims (encoders & decoders)" in { + // val foo = Foo("foo", 10) + // val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("issuer")))).value + // val claims = jwtVerifier.verifyJwt[Foo](jwt.token.toTokenP).value + // + // claims.payload shouldBe foo + // } + // + // "convert circe decoder to claims decoder and get error" in { + // val fooJson = """{"name":"Hello","age":"not number"}""" + // val jwt = JWT + // .create() + // .withPayload(unsafeParseJsonToJavaMap(fooJson)) + // .sign(Algorithm.HMAC256("secret")) + // val claims = jwtVerifier.verifyJwt[Foo](jwt.toTokenP) + // + // claims.left.value shouldBe JwtVerifyError.DecodingError("DecodingFailure at .age: Int", null) + // } + // + // "convert circe decoder to claims decoder and get error when format is incorrect" in { + // val fooJson = """{"name":,}""" + // + // summon[ClaimsDecoder[Foo]].decode(fooJson).left.value shouldEqual JwtVerifyError.DecodingError( + // "expected json value got ',}' (line 1, column 9)", + // ParseException("expected json value got ',}' (line 1, column 9)", 8, 1, 9), + // ) + // } + } +} diff --git a/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala b/oath/circe/src/test/scala/io/oath/circe/Foo.scala similarity index 92% rename from modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala rename to oath/circe/src/test/scala/io/oath/circe/Foo.scala index 7baa3cc..d39ed16 100644 --- a/modules/oath-circe/src/test/scala/io/oath/circe/Foo.scala +++ b/oath/circe/src/test/scala/io/oath/circe/Foo.scala @@ -5,5 +5,6 @@ import io.circe.generic.semiauto.* final case class Foo(name: String, age: Int) -object Foo: +object Foo { given barCodec: Codec[Foo] = deriveCodec[Foo] +} diff --git a/modules/oath-core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala b/oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala similarity index 90% rename from modules/oath-core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala rename to oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala index 0fdce24..419a962 100644 --- a/modules/oath-core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala +++ b/oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala @@ -1,4 +1,4 @@ -package io.oath.testkit +package io.oath.test import org.scalatest.matchers.should import org.scalatest.wordspec.AnyWordSpec diff --git a/modules/oath-core/src/test/scala/io/oath/testkit/Arbitraries.scala b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala similarity index 94% rename from modules/oath-core/src/test/scala/io/oath/testkit/Arbitraries.scala rename to oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala index 70ba7fa..d7c4c4f 100644 --- a/modules/oath-core/src/test/scala/io/oath/testkit/Arbitraries.scala +++ b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala @@ -1,19 +1,18 @@ -package io.oath.testkit +package io.oath.test import com.auth0.jwt.algorithms.Algorithm import io.oath.RegisteredClaims import io.oath.config.JwtIssuerConfig.RegisteredConfig import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} -import io.oath.config._ +import io.oath.config.* import io.oath.test.NestedHeader.SimpleHeader import io.oath.test.NestedPayload.SimplePayload -import io.oath.test.{NestedHeader, NestedPayload} -import org.scalacheck.{Arbitrary, Gen} +import org.scalacheck.* import java.time.Instant import scala.concurrent.duration.{Duration, DurationInt} -trait Arbitraries: +trait Arbitraries { lazy val genPositiveFiniteDuration = Gen.posNum[Long].map(Duration.fromNanos) lazy val genPositiveFiniteDurationSeconds = Gen.posNum[Int].map(x => (x + 1).seconds) @@ -28,11 +27,10 @@ trait Arbitraries: ) implicit val arbEncryptConfig: Arbitrary[EncryptConfig] = - Arbitrary: - arbNonEmptyString.arbitrary.map(EncryptConfig.apply) + Arbitrary(arbNonEmptyString.arbitrary.map(EncryptConfig.apply)) implicit val arbJwtIssuerConfig: Arbitrary[JwtIssuerConfig] = - Arbitrary: + Arbitrary { for { issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) @@ -52,9 +50,10 @@ trait Arbitraries: ) encrypt <- Gen.option(arbEncryptConfig.arbitrary) } yield JwtIssuerConfig(Algorithm.none(), encrypt, registered) + } implicit val arbJwtVerifierConfig: Arbitrary[JwtVerifierConfig] = - Arbitrary: + Arbitrary { for { encryptKey <- Gen.option(arbNonEmptyString.arbitrary) issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) @@ -68,9 +67,10 @@ trait Arbitraries: leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBefore) providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) } yield JwtVerifierConfig(Algorithm.none(), encrypt, providedWith, leewayWindow) + } implicit val arbJwtManagerConfig: Arbitrary[JwtManagerConfig] = - Arbitrary: + Arbitrary { for { encryptKey <- Gen.option(arbNonEmptyString.arbitrary) issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) @@ -98,9 +98,10 @@ trait Arbitraries: verifier = JwtVerifierConfig(Algorithm.none(), encrypt, providedWith, leewayWindow) issuer = JwtIssuerConfig(Algorithm.none(), encrypt, registered) } yield JwtManagerConfig(issuer, verifier) + } implicit val arbRegisteredClaims: Arbitrary[RegisteredClaims] = - Arbitrary: + Arbitrary { for { iss <- Gen.option(arbNonEmptyString.arbitrary) sub <- Gen.option(arbNonEmptyString.arbitrary) @@ -110,31 +111,37 @@ trait Arbitraries: iat <- Gen.option(arbInstant.arbitrary) jti <- Gen.option(arbNonEmptyString.arbitrary) } yield RegisteredClaims(iss, sub, aud, exp, nbf, iat, jti) + } implicit val arbSimplePayload: Arbitrary[SimplePayload] = - Arbitrary: + Arbitrary { for { name <- Gen.alphaStr data <- Gen.listOf(Gen.alphaStr) } yield SimplePayload(name, data) + } implicit val arbSimpleHeader: Arbitrary[SimpleHeader] = - Arbitrary: + Arbitrary { for { name <- Gen.alphaStr data <- Gen.listOf(Gen.alphaStr) } yield SimpleHeader(name, data) + } implicit val arbNestedPayload: Arbitrary[NestedPayload] = - Arbitrary: + Arbitrary { for { name <- Gen.alphaStr mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimplePayload.arbitrary.map((str, _)))) } yield NestedPayload(name, mapping) + } implicit val arbNestedHeader: Arbitrary[NestedHeader] = - Arbitrary: + Arbitrary { for { name <- Gen.alphaStr mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimpleHeader.arbitrary.map((str, _)))) } yield NestedHeader(name, mapping) + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/utils/ClockHelper.scala b/oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala similarity index 85% rename from modules/oath-core/src/test/scala/io/oath/utils/ClockHelper.scala rename to oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala index 1e4e2cd..833c68c 100644 --- a/modules/oath-core/src/test/scala/io/oath/utils/ClockHelper.scala +++ b/oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala @@ -1,9 +1,10 @@ -package io.oath.utils +package io.oath.test import java.time.temporal.ChronoUnit import java.time.{Clock, Instant, ZoneId} -trait ClockHelper: +trait ClockHelper { def getInstantNowSeconds: Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) def getFixedClock(time: Instant): Clock = Clock.fixed(time, ZoneId.of("UTC")) +} diff --git a/modules/oath-core/src/test/scala/io/oath/utils/CodecUtils.scala b/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala similarity index 84% rename from modules/oath-core/src/test/scala/io/oath/utils/CodecUtils.scala rename to oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala index 0e72615..9bedf7f 100644 --- a/modules/oath-core/src/test/scala/io/oath/utils/CodecUtils.scala +++ b/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala @@ -1,9 +1,10 @@ -package io.oath.utils +package io.oath.test import com.fasterxml.jackson.databind.ObjectMapper -trait CodecUtils: +trait CodecUtils { val mapper = new ObjectMapper def unsafeParseJsonToJavaMap(json: String): java.util.Map[String, Object] = mapper.readValue(json, classOf[java.util.HashMap[String, Object]]) +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/NestedHeader.scala b/oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala similarity index 87% rename from modules/oath-core/src/test/scala/io/oath/test/NestedHeader.scala rename to oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala index 0feee9d..b67267b 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/NestedHeader.scala +++ b/oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala @@ -8,21 +8,25 @@ import io.oath.* import io.oath.json.* import io.oath.test.NestedHeader.SimpleHeader -case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) +final case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) -object NestedHeader: - case class SimpleHeader(name: String, data: List[String]) +object NestedHeader { + final case class SimpleHeader(name: String, data: List[String]) given simpleHeaderCirceEncoder: Encoder[SimpleHeader] = deriveEncoder[SimpleHeader] + given simpleHeaderCirceDecoder: Decoder[SimpleHeader] = deriveDecoder[SimpleHeader] given nestedHeaderCirceEncoder: Encoder[NestedHeader] = deriveEncoder[NestedHeader] + given nestedHeaderCirceDecoder: Decoder[NestedHeader] = deriveDecoder[NestedHeader] given simpleHeaderEncoder: ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces + given simpleHeaderDecoder: ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") given nestedHeaderEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces + given nestedHeaderDecoder: ClaimsDecoder[NestedHeader] = nestedHeaderJson => parse(nestedHeaderJson).left .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) @@ -31,3 +35,4 @@ object NestedHeader: JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) ) ) +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/NestedPayload.scala b/oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala similarity index 90% rename from modules/oath-core/src/test/scala/io/oath/test/NestedPayload.scala rename to oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala index fb10403..58de79c 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/NestedPayload.scala +++ b/oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala @@ -8,10 +8,10 @@ import io.oath.* import io.oath.json.* import io.oath.test.NestedPayload.SimplePayload -case class NestedPayload(name: String, mapping: Map[String, SimplePayload]) +final case class NestedPayload(name: String, mapping: Map[String, SimplePayload]) object NestedPayload: - case class SimplePayload(name: String, data: List[String]) + final case class SimplePayload(name: String, data: List[String]) given simplePayloadCirceEncoder: Encoder[SimplePayload] = deriveEncoder[SimplePayload] given simplePayloadCirceDecoder: Decoder[SimplePayload] = deriveDecoder[SimplePayload] diff --git a/modules/oath-core/src/test/scala/io/oath/test/OathToken.scala b/oath/core-test/src/main/scala/io/oath/test/OathToken.scala similarity index 83% rename from modules/oath-core/src/test/scala/io/oath/test/OathToken.scala rename to oath/core-test/src/main/scala/io/oath/test/OathToken.scala index 23bbb19..0bcadb6 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/OathToken.scala +++ b/oath/core-test/src/main/scala/io/oath/test/OathToken.scala @@ -1,4 +1,5 @@ package io.oath.test -enum OathToken: +enum OathToken { case AccessToken, RefreshToken, ActivationEmailToken, ForgotPasswordToken +} diff --git a/modules/oath-core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala b/oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala similarity index 90% rename from modules/oath-core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala rename to oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala index 3ec7c82..bbfe022 100644 --- a/modules/oath-core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala +++ b/oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala @@ -1,10 +1,11 @@ -package io.oath.testkit +package io.oath.test import org.scalactic.anyvals.PosInt import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks -trait PropertyBasedTesting extends ScalaCheckPropertyChecks, Arbitraries: +trait PropertyBasedTesting extends ScalaCheckPropertyChecks, Arbitraries { val minSuccessful = PosInt(25) override implicit val generatorDrivenConfig: PropertyCheckConfiguration = PropertyCheckConfiguration(minSuccessful) +} diff --git a/modules/oath-core/src/test/scala/io/oath/utils/TestData.scala b/oath/core-test/src/main/scala/io/oath/test/TestData.scala similarity index 87% rename from modules/oath-core/src/test/scala/io/oath/utils/TestData.scala rename to oath/core-test/src/main/scala/io/oath/test/TestData.scala index 579bbf5..bc5d16e 100644 --- a/modules/oath-core/src/test/scala/io/oath/utils/TestData.scala +++ b/oath/core-test/src/main/scala/io/oath/test/TestData.scala @@ -1,4 +1,4 @@ -package io.oath.utils +package io.oath.test import com.auth0.jwt.JWTCreator import io.oath.RegisteredClaims diff --git a/modules/oath-core/src/test/resources/algorithm-es256.conf b/oath/core-test/src/test/resources/algorithm-es256.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-es256.conf rename to oath/core-test/src/test/resources/algorithm-es256.conf diff --git a/modules/oath-core/src/test/resources/algorithm-es384.conf b/oath/core-test/src/test/resources/algorithm-es384.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-es384.conf rename to oath/core-test/src/test/resources/algorithm-es384.conf diff --git a/modules/oath-core/src/test/resources/algorithm-es512.conf b/oath/core-test/src/test/resources/algorithm-es512.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-es512.conf rename to oath/core-test/src/test/resources/algorithm-es512.conf diff --git a/modules/oath-core/src/test/resources/algorithm-hsxxx.conf b/oath/core-test/src/test/resources/algorithm-hsxxx.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-hsxxx.conf rename to oath/core-test/src/test/resources/algorithm-hsxxx.conf diff --git a/modules/oath-core/src/test/resources/algorithm-none.conf b/oath/core-test/src/test/resources/algorithm-none.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-none.conf rename to oath/core-test/src/test/resources/algorithm-none.conf diff --git a/modules/oath-core/src/test/resources/algorithm-rsxxx.conf b/oath/core-test/src/test/resources/algorithm-rsxxx.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-rsxxx.conf rename to oath/core-test/src/test/resources/algorithm-rsxxx.conf diff --git a/modules/oath-core/src/test/resources/algorithm-unsupported.conf b/oath/core-test/src/test/resources/algorithm-unsupported.conf similarity index 100% rename from modules/oath-core/src/test/resources/algorithm-unsupported.conf rename to oath/core-test/src/test/resources/algorithm-unsupported.conf diff --git a/modules/oath-core/src/test/resources/issuer.conf b/oath/core-test/src/test/resources/issuer.conf similarity index 100% rename from modules/oath-core/src/test/resources/issuer.conf rename to oath/core-test/src/test/resources/issuer.conf diff --git a/modules/oath-core/src/test/resources/manager.conf b/oath/core-test/src/test/resources/manager.conf similarity index 100% rename from modules/oath-core/src/test/resources/manager.conf rename to oath/core-test/src/test/resources/manager.conf diff --git a/modules/oath-core/src/test/resources/reference.conf b/oath/core-test/src/test/resources/reference.conf similarity index 100% rename from modules/oath-core/src/test/resources/reference.conf rename to oath/core-test/src/test/resources/reference.conf diff --git a/modules/oath-core/src/test/resources/verifier.conf b/oath/core-test/src/test/resources/verifier.conf similarity index 100% rename from modules/oath-core/src/test/resources/verifier.conf rename to oath/core-test/src/test/resources/verifier.conf diff --git a/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala similarity index 99% rename from modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala rename to oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala index f6d07f9..129f4a8 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/JwtIssuerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -1,13 +1,12 @@ -package io.oath.test +package io.oath import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.oath.* import io.oath.config.* -import io.oath.syntax.* +import io.oath.syntax.all.* import io.oath.test.NestedHeader.nestedHeaderDecoder import io.oath.test.NestedPayload.nestedPayloadDecoder -import io.oath.testkit.* +import io.oath.test.* import io.oath.utils.* import scala.concurrent.duration.DurationInt @@ -15,7 +14,7 @@ import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.Try import scala.util.chaining.scalaUtilChainingOps -class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: +class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val jwtVerifier = JWT .require(Algorithm.none()) @@ -231,3 +230,4 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper: } } } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/JwtManagerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala similarity index 88% rename from modules/oath-core/src/test/scala/io/oath/test/JwtManagerSpec.scala rename to oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala index dc5f90f..716ee63 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/JwtManagerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala @@ -1,20 +1,20 @@ -package io.oath.test +package io.oath -import io.oath.* import io.oath.config.* -import io.oath.syntax.* -import io.oath.testkit.* +import io.oath.syntax.all.* +import io.oath.test.* -class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting: +class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting { - "JwtManager" should: - "be able to issue and verify jwt tokens without claims" in forAll: (config: JwtManagerConfig) => + "JwtManager" should { + "be able to issue and verify jwt tokens without claims" in forAll { (config: JwtManagerConfig) => val jwtManager = new JwtManager(config) val jwt = jwtManager.issueJwt().value jwtManager.verifyJwt(jwt.token.toToken).value.registered shouldBe jwt.claims.registered + } - "be able to issue and verify jwt tokens with header claims" in forAll: + "be able to issue and verify jwt tokens with header claims" in forAll { (config: JwtManagerConfig, nestedHeader: NestedHeader) => val jwtManager = new JwtManager(config) @@ -22,8 +22,9 @@ class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting: val jwt = jwtManager.issueJwt(claims).value jwtManager.verifyJwt[NestedHeader](jwt.token.toTokenH).value shouldBe claims .copy(registered = jwt.claims.registered) + } - "be able to issue and verify jwt tokens with payload claims" in forAll: + "be able to issue and verify jwt tokens with payload claims" in forAll { (config: JwtManagerConfig, nestedPayload: NestedPayload) => val jwtManager = new JwtManager(config) @@ -31,8 +32,9 @@ class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting: val jwt = jwtManager.issueJwt(claims).value jwtManager.verifyJwt[NestedPayload](jwt.token.toTokenP).value shouldBe claims .copy(registered = jwt.claims.registered) + } - "be able to issue and verify jwt tokens with header & payload claims" in forAll: + "be able to issue and verify jwt tokens with header & payload claims" in forAll { (config: JwtManagerConfig, nestedHeader: NestedHeader, nestedPayload: NestedPayload) => val jwtManager = new JwtManager(config) @@ -40,3 +42,6 @@ class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting: val jwt = jwtManager.issueJwt(claims).value jwtManager.verifyJwt[NestedHeader, NestedPayload](jwt.token.toTokenHP).value shouldBe claims .copy(registered = jwt.claims.registered) + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala similarity index 98% rename from modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala rename to oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala index f6c1bf2..11e96fb 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/JwtVerifierSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -1,20 +1,19 @@ -package io.oath.test +package io.oath import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.exceptions.* import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.* import io.oath.config.JwtVerifierConfig.* import io.oath.config.{EncryptConfig, JwtVerifierConfig} -import io.oath.syntax.* +import io.oath.syntax.all.* import io.oath.test.NestedHeader.{SimpleHeader, nestedHeaderEncoder} import io.oath.test.NestedPayload.{SimplePayload, nestedPayloadEncoder} -import io.oath.testkit.{AnyWordSpecBase, PropertyBasedTesting} +import io.oath.test.* import io.oath.utils.* import scala.util.chaining.scalaUtilChainingOps -class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper, CodecUtils: +class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper, CodecUtils { val defaultConfig = JwtVerifierConfig( @@ -44,7 +43,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val builderWithRegistered = builder .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) - .tap(builder => builder.withAudience(registeredClaims.aud: _*)) + .tap(builder => builder.withAudience(registeredClaims.aud*)) .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) @@ -380,3 +379,4 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper .VerificationError("JWT Token is empty.") } } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/OathIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala similarity index 84% rename from modules/oath-core/src/test/scala/io/oath/test/OathIssuerSpec.scala rename to oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala index 89c5260..99ef530 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/OathIssuerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala @@ -1,12 +1,11 @@ -package io.oath.test +package io.oath -import io.oath.* -import io.oath.testkit.AnyWordSpecBase +import io.oath.test.* -class OathIssuerSpec extends AnyWordSpecBase: +class OathIssuerSpec extends AnyWordSpecBase { - "OathIssuer" should: - "create jwt token issuers" in: + "OathIssuer" should { + "create jwt token issuers" in { inline def oathIssuer = OathIssuer.createOrFail[OathToken] val accessTokenIssuer: JIssuer[OathToken.AccessToken.type] = oathIssuer.as(OathToken.AccessToken) @@ -20,3 +19,6 @@ class OathIssuerSpec extends AnyWordSpecBase: refreshTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("refresh-token") activationEmailTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("activation-email-token") forgotPasswordTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("forgot-password-token") + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/OathManagerSpec.scala b/oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala similarity index 85% rename from modules/oath-core/src/test/scala/io/oath/test/OathManagerSpec.scala rename to oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala index d8dcc9d..886cc95 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/OathManagerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala @@ -1,13 +1,12 @@ -package io.oath.test +package io.oath -import io.oath.* -import io.oath.syntax.* -import io.oath.testkit.AnyWordSpecBase +import io.oath.syntax.all.* +import io.oath.test.* -class OathManagerSpec extends AnyWordSpecBase: +class OathManagerSpec extends AnyWordSpecBase { - "OathManager" should: - "create different token managers" in: + "OathManager" should { + "create different token managers" in { val oathManager = OathManager.createOrFail[OathToken] val accessTokenManager: JManager[OathToken.AccessToken.type] = oathManager.as(OathToken.AccessToken) @@ -26,3 +25,6 @@ class OathManagerSpec extends AnyWordSpecBase: refreshTokenManager.verifyJwt(refreshToken.toToken).isRight shouldBe true activationEmailTokenManager.verifyJwt(activationEmailToken.toToken).isRight shouldBe true forgotPasswordTokenManager.verifyJwt(forgotPasswordToken.toToken).isRight shouldBe true + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/OathVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala similarity index 89% rename from modules/oath-core/src/test/scala/io/oath/test/OathVerifierSpec.scala rename to oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala index d17409b..49392e7 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/OathVerifierSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala @@ -1,10 +1,9 @@ -package io.oath.test +package io.oath -import io.oath.* -import io.oath.syntax.* -import io.oath.testkit.AnyWordSpecBase +import io.oath.syntax.all.* +import io.oath.test.* -class OathVerifierSpec extends AnyWordSpecBase: +class OathVerifierSpec extends AnyWordSpecBase { val oathIssuer = OathIssuer.createOrFail[OathToken] @@ -15,9 +14,8 @@ class OathVerifierSpec extends AnyWordSpecBase: val forgotPasswordTokenIssuer: JIssuer[OathToken.ForgotPasswordToken.type] = oathIssuer.as(OathToken.ForgotPasswordToken) - "OathVerifier" should: - - "create different token verifiers" in: + "OathVerifier" should { + "create different token verifiers" in { val oathVerifier = OathVerifier.createOrFail[OathToken] val accessTokenVerifier: JVerifier[OathToken.AccessToken.type] = oathVerifier.as(OathToken.AccessToken) @@ -36,3 +34,6 @@ class OathVerifierSpec extends AnyWordSpecBase: refreshTokenVerifier.verifyJwt(refreshToken.toToken).isRight shouldBe true activationEmailTokenVerifier.verifyJwt(activationEmailToken.toToken).isRight shouldBe true forgotPasswordTokenVerifier.verifyJwt(forgotPasswordToken.toToken).isRight shouldBe true + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/config/AlgorithmLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala similarity index 84% rename from modules/oath-core/src/test/scala/io/oath/test/config/AlgorithmLoaderSpec.scala rename to oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala index 1f9eac3..7e874cd 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/config/AlgorithmLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -1,15 +1,14 @@ -package io.oath.test.config +package io.oath.config import com.auth0.jwt.JWT import com.typesafe.config.ConfigFactory -import io.oath.config.AlgorithmLoader -import io.oath.testkit.{AnyWordSpecBase, PropertyBasedTesting} +import io.oath.test.* -class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: - private val AlgorithmConfigLocation = "algorithm" +class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { + inline val AlgorithmConfigLocation = "algorithm" - "AlgorithmLoader" should: - "load none encryption algorithm config" in forAll: (issuer: String) => + "AlgorithmLoader" should { + "load none encryption algorithm config" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-none").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -21,8 +20,9 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "none" verifiedIssuer shouldBe issuer token should not be empty + } - "load RSXXX encryption algorithm with secret key" in forAll: (issuer: String) => + "load RSXXX encryption algorithm with secret key" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-rsxxx").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -34,8 +34,9 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "RS256" verifiedIssuer shouldBe issuer token should not be empty + } - "load HSXXX encryption algorithm with secret key" in forAll: (issuer: String) => + "load HSXXX encryption algorithm with secret key" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-hsxxx").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -47,8 +48,9 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "HS256" verifiedIssuer shouldBe issuer token should not be empty + } - "load ES256 encryption algorithm with secret key" in forAll: (issuer: String) => + "load ES256 encryption algorithm with secret key" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-es256").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -60,8 +62,9 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "ES256" verifiedIssuer shouldBe issuer token should not be empty + } - "load ES384 encryption algorithm with secret key" in forAll: (issuer: String) => + "load ES384 encryption algorithm with secret key" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-es384").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -73,8 +76,9 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "ES384" verifiedIssuer shouldBe issuer token should not be empty + } - "load ES512 encryption algorithm with secret key" in forAll: (issuer: String) => + "load ES512 encryption algorithm with secret key" in forAll { (issuer: String) => val algorithmScopedConfig = ConfigFactory.load("algorithm-es512").getConfig(AlgorithmConfigLocation) val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) @@ -86,8 +90,12 @@ class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting: verifyingAlgorithm.getName shouldBe "ES512" verifiedIssuer shouldBe issuer token should not be empty + } - "fail to load unsupported algorithm type" in forAll: (bool: Boolean) => + "fail to load unsupported algorithm type" in forAll { (bool: Boolean) => val algorithmScopedConfig = ConfigFactory.load("algorithm-unsupported").getConfig(AlgorithmConfigLocation) the[IllegalArgumentException] thrownBy AlgorithmLoader .loadOrThrow(algorithmScopedConfig, bool) should have message "Unsupported signature algorithm: Boom" + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/config/JwtIssuerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala similarity index 81% rename from modules/oath-core/src/test/scala/io/oath/test/config/JwtIssuerLoaderSpec.scala rename to oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index 9b10beb..a347540 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/config/JwtIssuerLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -1,23 +1,22 @@ -package io.oath.test.config +package io.oath.config import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.config.{EncryptConfig, JwtIssuerConfig} -import io.oath.testkit.AnyWordSpecBase +import io.oath.test.* import scala.concurrent.duration.DurationInt -class JwtIssuerLoaderSpec extends AnyWordSpecBase: +class JwtIssuerLoaderSpec extends AnyWordSpecBase { - val configFile = "issuer" - val DefaultTokenConfigLocation = "default-token" - val TokenConfigLocation = "token" - val TokenWithEncryptionConfigLocation = "token-with-encryption" - val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" - val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" - val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + inline val configFile = "issuer" + inline val DefaultTokenConfigLocation = "default-token" + inline val TokenConfigLocation = "token" + inline val TokenWithEncryptionConfigLocation = "token-with-encryption" + inline val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" + inline val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" + inline val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" - "IssuerLoader" should: - "load default-token issuer config values from configuration file" in: + "IssuerLoader" should { + "load default-token issuer config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) val config = JwtIssuerConfig.loadOrThrow(configLoader) @@ -30,8 +29,9 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase: config.registered.expiresAtOffset shouldBe None config.registered.notBeforeOffset shouldBe None config.algorithm.getName shouldBe "HS256" + } - "load token issuer config values from configuration file" in: + "load token issuer config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) val config = JwtIssuerConfig.loadOrThrow(configLoader) @@ -44,8 +44,9 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase: config.registered.expiresAtOffset shouldBe Some(1.day) config.registered.notBeforeOffset shouldBe Some(1.minute) config.algorithm.getName shouldBe "RS256" + } - "load token issuer config values from configuration file with encryption key" in: + "load token issuer config values from configuration file with encryption key" in { val configLoader = ConfigFactory.load(configFile).getConfig(TokenWithEncryptionConfigLocation) val config = JwtIssuerConfig.loadOrThrow(configLoader) @@ -58,8 +59,9 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase: config.registered.expiresAtOffset shouldBe Some(1.day) config.registered.notBeforeOffset shouldBe Some(1.minute) config.algorithm.getName shouldBe "RS256" + } - "load token issuer config values from reference configuration file using location" in: + "load token issuer config values from reference configuration file using location" in { val config = JwtIssuerConfig.loadOrThrow(TokenConfigLocation) config.encrypt shouldBe empty @@ -71,18 +73,24 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase: config.registered.expiresAtOffset shouldBe Some(1.day) config.registered.notBeforeOffset shouldBe Some(1.minute) config.algorithm.getName shouldBe "RS256" + } - "fail to load without-private-key-token issuer config values from configuration file" in: + "fail to load without-private-key-token issuer config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPrivateKeyTokenConfigLocation) the[ConfigException.Missing] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } - "fail to load invalid-token-empty-string issuer config values from configuration file" in: + "fail to load invalid-token-empty-string issuer config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) the[IllegalArgumentException] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } - "fail to load invalid-token-wrong-type issuer config values from configuration file" in: + "fail to load invalid-token-wrong-type issuer config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) the[ConfigException.BadValue] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/config/JwtManagerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala similarity index 88% rename from modules/oath-core/src/test/scala/io/oath/test/config/JwtManagerLoaderSpec.scala rename to oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala index baf3f67..7942906 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/config/JwtManagerLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala @@ -1,18 +1,17 @@ -package io.oath.test.config +package io.oath.config import com.typesafe.config.ConfigFactory -import io.oath.config.JwtManagerConfig -import io.oath.testkit.AnyWordSpecBase +import io.oath.test.* import scala.concurrent.duration.DurationInt -class JwtManagerLoaderSpec extends AnyWordSpecBase: +class JwtManagerLoaderSpec extends AnyWordSpecBase { val configFile = "manager" val TokenConfigLocation = "token" - "ManagerLoader" should: - "load default-token verifier config values from configuration file" in: + "ManagerLoader" should { + "load default-token verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) val config = JwtManagerConfig.loadOrThrow(configLoader) @@ -33,3 +32,6 @@ class JwtManagerLoaderSpec extends AnyWordSpecBase: config.verifier.leewayWindow.expiresAt shouldBe Some(3.minutes) config.verifier.leewayWindow.notBefore shouldBe Some(2.minutes) config.verifier.algorithm.getName shouldBe "RS256" + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/config/JwtVerifierLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala similarity index 92% rename from modules/oath-core/src/test/scala/io/oath/test/config/JwtVerifierLoaderSpec.scala rename to oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala index cff83cc..0330b73 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/config/JwtVerifierLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -1,12 +1,11 @@ -package io.oath.test.config +package io.oath.config import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.config.{EncryptConfig, JwtVerifierConfig} -import io.oath.testkit.AnyWordSpecBase +import io.oath.test.* import scala.concurrent.duration.DurationInt -class JwtVerifierLoaderSpec extends AnyWordSpecBase: +class JwtVerifierLoaderSpec extends AnyWordSpecBase { val configFile = "verifier" val DefaultTokenConfigLocation = "default-token" @@ -16,8 +15,8 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase: val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" - "VerifierLoader" should: - "load default-token verifier config values from configuration file" in: + "VerifierLoader" should { + "load default-token verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) val config = JwtVerifierConfig.loadOrThrow(configLoader) @@ -31,8 +30,9 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase: config.leewayWindow.expiresAt shouldBe None config.leewayWindow.notBefore shouldBe None config.algorithm.getName shouldBe "HS256" + } - "load token verifier config values from configuration file" in: + "load token verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) val config = JwtVerifierConfig.loadOrThrow(configLoader) @@ -45,8 +45,9 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase: config.leewayWindow.expiresAt shouldBe Some(3.minutes) config.leewayWindow.notBefore shouldBe Some(2.minutes) config.algorithm.getName shouldBe "RS256" + } - "load token verifier config values from configuration file with encryption" in: + "load token verifier config values from configuration file with encryption" in { val configLoader = ConfigFactory.load(configFile).getConfig(TokenWithEncryptionConfigLocation) val config = JwtVerifierConfig.loadOrThrow(configLoader) @@ -59,8 +60,9 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase: config.leewayWindow.expiresAt shouldBe Some(3.minutes) config.leewayWindow.notBefore shouldBe Some(2.minutes) config.algorithm.getName shouldBe "RS256" + } - "load token verifier config values from reference configuration file using location" in: + "load token verifier config values from reference configuration file using location" in { val config = JwtVerifierConfig.loadOrThrow(TokenConfigLocation) config.encrypt shouldBe empty @@ -72,18 +74,24 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase: config.leewayWindow.expiresAt shouldBe Some(3.minutes) config.leewayWindow.notBefore shouldBe Some(2.minutes) config.algorithm.getName shouldBe "RS256" + } - "fail to load without-public-key-token verifier config values from configuration file" in: + "fail to load without-public-key-token verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPublicKeyTokenConfigLocation) the[ConfigException.Missing] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } - "fail to load invalid-token-empty-string verifier config values from configuration file" in: + "fail to load invalid-token-empty-string verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) the[IllegalArgumentException] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } - "fail to load invalid-token-wrong-type verifier config values from configuration file" in: + "fail to load invalid-token-wrong-type verifier config values from configuration file" in { val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) the[ConfigException.WrongType] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } + } +} diff --git a/modules/oath-core/src/test/scala/io/oath/test/utils/UtilsSpec.scala b/oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala similarity index 69% rename from modules/oath-core/src/test/scala/io/oath/test/utils/UtilsSpec.scala rename to oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala index 8aaca61..99de717 100644 --- a/modules/oath-core/src/test/scala/io/oath/test/utils/UtilsSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala @@ -1,12 +1,12 @@ -package io.oath.test.utils +package io.oath.utils -import io.oath.testkit.AnyWordSpecBase +import io.oath.test.* import io.oath.utils.* -class UtilsSpec extends AnyWordSpecBase: +class UtilsSpec extends AnyWordSpecBase { - "FormatConversion" should: - "convert upper camel case to lower hyphen" in: + "FormatConversion" should { + "convert upper camel case to lower hyphen" in { val res1 = convertUpperCamelToLowerHyphen("HelloWorld") val res2 = convertUpperCamelToLowerHyphen(" Hello World ") @@ -14,8 +14,9 @@ class UtilsSpec extends AnyWordSpecBase: res1 shouldBe expected res2 shouldBe expected + } - "convert scala enum string values to lower hyphen" in: + "convert scala enum string values to lower hyphen" in { enum SomeEnum: case firstEnum, SecondEnum, Third, ForthEnumValue @@ -24,3 +25,6 @@ class UtilsSpec extends AnyWordSpecBase: SomeEnum.values.toSeq .map(_.toString) .map(convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected + } + } +} diff --git a/modules/oath-core/src/test/secrets/es256-private.pem b/oath/core-test/src/test/secrets/es256-private.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es256-private.pem rename to oath/core-test/src/test/secrets/es256-private.pem diff --git a/modules/oath-core/src/test/secrets/es256-public.pem b/oath/core-test/src/test/secrets/es256-public.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es256-public.pem rename to oath/core-test/src/test/secrets/es256-public.pem diff --git a/modules/oath-core/src/test/secrets/es384-private.pem b/oath/core-test/src/test/secrets/es384-private.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es384-private.pem rename to oath/core-test/src/test/secrets/es384-private.pem diff --git a/modules/oath-core/src/test/secrets/es384-public.pem b/oath/core-test/src/test/secrets/es384-public.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es384-public.pem rename to oath/core-test/src/test/secrets/es384-public.pem diff --git a/modules/oath-core/src/test/secrets/es512-private.pem b/oath/core-test/src/test/secrets/es512-private.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es512-private.pem rename to oath/core-test/src/test/secrets/es512-private.pem diff --git a/modules/oath-core/src/test/secrets/es512-public.pem b/oath/core-test/src/test/secrets/es512-public.pem similarity index 100% rename from modules/oath-core/src/test/secrets/es512-public.pem rename to oath/core-test/src/test/secrets/es512-public.pem diff --git a/modules/oath-core/src/test/secrets/rsa-private.pem b/oath/core-test/src/test/secrets/rsa-private.pem similarity index 100% rename from modules/oath-core/src/test/secrets/rsa-private.pem rename to oath/core-test/src/test/secrets/rsa-private.pem diff --git a/modules/oath-core/src/test/secrets/rsa-public.pem b/oath/core-test/src/test/secrets/rsa-public.pem similarity index 100% rename from modules/oath-core/src/test/secrets/rsa-public.pem rename to oath/core-test/src/test/secrets/rsa-public.pem diff --git a/modules/oath-core/src/main/scala/io/oath/Jwt.scala b/oath/core/src/main/scala/io/oath/Jwt.scala similarity index 100% rename from modules/oath-core/src/main/scala/io/oath/Jwt.scala rename to oath/core/src/main/scala/io/oath/Jwt.scala diff --git a/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala b/oath/core/src/main/scala/io/oath/JwtClaims.scala similarity index 91% rename from modules/oath-core/src/main/scala/io/oath/JwtClaims.scala rename to oath/core/src/main/scala/io/oath/JwtClaims.scala index 93e6672..7bcfaff 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtClaims.scala +++ b/oath/core/src/main/scala/io/oath/JwtClaims.scala @@ -1,9 +1,10 @@ package io.oath -sealed trait JwtClaims: +sealed trait JwtClaims { val registered: RegisteredClaims +} -object JwtClaims: +object JwtClaims { final case class Claims(registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims final case class ClaimsH[+H](header: H, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims @@ -12,3 +13,4 @@ object JwtClaims: final case class ClaimsHP[+H, +P](header: H, payload: P, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala b/oath/core/src/main/scala/io/oath/JwtIssueError.scala similarity index 52% rename from modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala rename to oath/core/src/main/scala/io/oath/JwtIssueError.scala index d1eda0e..529a60f 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtIssueError.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssueError.scala @@ -1,11 +1,20 @@ package io.oath -sealed abstract class JwtIssueError(error: String, cause: Throwable = null) extends Exception(error, cause) +import cats.syntax.all.* + +sealed abstract class JwtIssueError(error: String, cause: Option[Throwable] = None) + extends Exception(error, cause.orNull) + +object JwtIssueError { + case class IllegalArgument(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) + + case class JwtCreationIssueError(message: String, underlying: Throwable) + extends JwtIssueError(message, underlying.some) -object JwtIssueError: - case class IllegalArgument(message: String, underlying: Throwable) extends JwtIssueError(message, underlying) - case class JwtCreationIssueError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying) case class EncryptionError(message: String) extends JwtIssueError(message) + case class EncodeError(message: String) extends JwtIssueError(message) + case class UnexpectedIssueError(message: String, underlying: Option[Throwable] = None) - extends JwtIssueError(message, underlying.orNull) + extends JwtIssueError(message, underlying) +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala similarity index 98% rename from modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala rename to oath/core/src/main/scala/io/oath/JwtIssuer.scala index 9baaf09..f321b69 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -14,13 +14,13 @@ import java.util.UUID import scala.util.chaining.* import scala.util.control.Exception.allCatch -final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()): +final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) { private def buildJwt(builder: JWTCreator.Builder, registeredClaims: RegisteredClaims): JWTCreator.Builder = builder .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) - .tap(builder => builder.withAudience(registeredClaims.aud: _*)) + .tap(builder => builder.withAudience(registeredClaims.aud*)) .tap(builder => registeredClaims.jti.map(str => builder.withJWTId(str))) .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) @@ -142,3 +142,4 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) encryptedJwt <- maybeEncryptJwt(jwt) yield encryptedJwt } +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtManager.scala b/oath/core/src/main/scala/io/oath/JwtManager.scala similarity index 100% rename from modules/oath-core/src/main/scala/io/oath/JwtManager.scala rename to oath/core/src/main/scala/io/oath/JwtManager.scala diff --git a/modules/oath-core/src/main/scala/io/oath/JwtToken.scala b/oath/core/src/main/scala/io/oath/JwtToken.scala similarity index 92% rename from modules/oath-core/src/main/scala/io/oath/JwtToken.scala rename to oath/core/src/main/scala/io/oath/JwtToken.scala index 1b989a4..3da9fb6 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtToken.scala +++ b/oath/core/src/main/scala/io/oath/JwtToken.scala @@ -3,8 +3,12 @@ package io.oath sealed trait JwtToken: def token: String -object JwtToken: +object JwtToken { case class Token(token: String) extends JwtToken + case class TokenH(token: String) extends JwtToken + case class TokenP(token: String) extends JwtToken + case class TokenHP(token: String) extends JwtToken +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala b/oath/core/src/main/scala/io/oath/JwtVerifier.scala similarity index 98% rename from modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala rename to oath/core/src/main/scala/io/oath/JwtVerifier.scala index 0777c2a..363cb58 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtVerifier.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifier.scala @@ -11,7 +11,7 @@ import io.oath.utils.* import scala.util.chaining.scalaUtilChainingOps import scala.util.control.Exception.allCatch -final class JwtVerifier(config: JwtVerifierConfig): +final class JwtVerifier(config: JwtVerifierConfig) { private lazy val jwtVerifier = JWT @@ -20,7 +20,7 @@ final class JwtVerifier(config: JwtVerifierConfig): .tap(jwtVerification => config.providedWith.subjectClaim.map(str => jwtVerification.withSubject(str))) .tap(jwtVerification => if (config.providedWith.audienceClaims.nonEmpty) - jwtVerification.withAudience(config.providedWith.audienceClaims: _*) + jwtVerification.withAudience(config.providedWith.audienceClaims*) else () ) .tap(jwtVerification => @@ -130,3 +130,4 @@ final class JwtVerifier(config: JwtVerifierConfig): payloadClaims <- safeDecode(payloadDecoder.decode(jsonPayload)) registeredClaims = getRegisteredClaims(decodedJwt) yield JwtClaims.ClaimsHP(headerClaims, payloadClaims, registeredClaims) +} diff --git a/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala similarity index 57% rename from modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala rename to oath/core/src/main/scala/io/oath/JwtVerifyError.scala index e102566..9ba7876 100644 --- a/modules/oath-core/src/main/scala/io/oath/JwtVerifyError.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala @@ -1,17 +1,28 @@ package io.oath -sealed abstract class JwtVerifyError(error: String, cause: Throwable = null) extends Exception(error, cause) +import cats.syntax.all.* + +sealed abstract class JwtVerifyError(error: String, cause: Option[Throwable] = None) + extends Exception(error, cause.orNull) + +object JwtVerifyError { + case class IllegalArgument(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) + + case class AlgorithmMismatch(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) + + case class DecodingError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) -object JwtVerifyError: - case class IllegalArgument(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) - case class AlgorithmMismatch(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) - case class DecodingError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) case class VerificationError(message: String, underlying: Option[Throwable] = None) - extends JwtVerifyError(message, underlying.orNull) - case class SignatureVerificationError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying) + + case class SignatureVerificationError(message: String, underlying: Throwable) + extends JwtVerifyError(message, underlying.some) + case class DecryptionError(message: String) extends JwtVerifyError(message) + case class TokenExpired(message: String, underlying: Option[Throwable] = None) - extends JwtVerifyError(message, underlying.orNull) + extends JwtVerifyError(message, underlying) + case class UnexpectedError(message: String, underlying: Option[Throwable] = None) - extends JwtVerifyError(message, underlying.orNull) + extends JwtVerifyError(message, underlying) +} diff --git a/modules/oath-core/src/main/scala/io/oath/OathIssuer.scala b/oath/core/src/main/scala/io/oath/OathIssuer.scala similarity index 88% rename from modules/oath-core/src/main/scala/io/oath/OathIssuer.scala rename to oath/core/src/main/scala/io/oath/OathIssuer.scala index 0ac29ee..cc70ded 100644 --- a/modules/oath-core/src/main/scala/io/oath/OathIssuer.scala +++ b/oath/core/src/main/scala/io/oath/OathIssuer.scala @@ -4,10 +4,11 @@ import io.oath.config.JwtIssuerConfig import scala.util.chaining.scalaUtilChainingOps -final class OathIssuer[A](mapping: Map[A, JwtIssuer]): +final class OathIssuer[A](mapping: Map[A, JwtIssuer]) { def as[S <: A](tokenType: S): JIssuer[S] = mapping(tokenType) +} -object OathIssuer: +object OathIssuer { inline def none[A]: OathIssuer[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtIssuer(JwtIssuerConfig.none()) @@ -19,3 +20,4 @@ object OathIssuer: tokenType -> JwtIssuerConfig.loadOrThrowOath(tokenConfig).pipe(JwtIssuer(_)) }.toMap .pipe(mapping => OathIssuer(mapping)) +} diff --git a/modules/oath-core/src/main/scala/io/oath/OathManager.scala b/oath/core/src/main/scala/io/oath/OathManager.scala similarity index 87% rename from modules/oath-core/src/main/scala/io/oath/OathManager.scala rename to oath/core/src/main/scala/io/oath/OathManager.scala index 72a95ce..25aca0e 100644 --- a/modules/oath-core/src/main/scala/io/oath/OathManager.scala +++ b/oath/core/src/main/scala/io/oath/OathManager.scala @@ -4,10 +4,11 @@ import io.oath.config.* import scala.util.chaining.scalaUtilChainingOps -final class OathManager[A](mapping: Map[A, JwtManager]): +final class OathManager[A](mapping: Map[A, JwtManager]) { def as[S <: A](tokenType: S): JManager[S] = mapping(tokenType) +} -object OathManager: +object OathManager { inline def none[A]: OathManager[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtManager(JwtManagerConfig.none()) @@ -19,3 +20,4 @@ object OathManager: tokenType -> JwtManagerConfig.loadOrThrowOath(tokenConfig).pipe(JwtManager(_)) }.toMap .pipe(mapping => OathManager(mapping)) +} diff --git a/modules/oath-core/src/main/scala/io/oath/OathVerifier.scala b/oath/core/src/main/scala/io/oath/OathVerifier.scala similarity index 96% rename from modules/oath-core/src/main/scala/io/oath/OathVerifier.scala rename to oath/core/src/main/scala/io/oath/OathVerifier.scala index 7e198bd..7698056 100644 --- a/modules/oath-core/src/main/scala/io/oath/OathVerifier.scala +++ b/oath/core/src/main/scala/io/oath/OathVerifier.scala @@ -7,7 +7,7 @@ import scala.util.chaining.scalaUtilChainingOps final class OathVerifier[A](mapping: Map[A, JwtVerifier]): def as[S <: A](tokenType: S): JVerifier[S] = mapping(tokenType) -object OathVerifier: +object OathVerifier { inline def none[A]: OathVerifier[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtVerifier(JwtVerifierConfig.none()) @@ -19,3 +19,4 @@ object OathVerifier: tokenType -> JwtVerifierConfig.loadOrThrowOath(tokenConfig).pipe(JwtVerifier(_)) }.toMap .pipe(mapping => OathVerifier(mapping)) +} diff --git a/modules/oath-core/src/main/scala/io/oath/RegisteredClaims.scala b/oath/core/src/main/scala/io/oath/RegisteredClaims.scala similarity index 84% rename from modules/oath-core/src/main/scala/io/oath/RegisteredClaims.scala rename to oath/core/src/main/scala/io/oath/RegisteredClaims.scala index 2574bda..6781d54 100644 --- a/modules/oath-core/src/main/scala/io/oath/RegisteredClaims.scala +++ b/oath/core/src/main/scala/io/oath/RegisteredClaims.scala @@ -2,7 +2,7 @@ package io.oath import java.time.Instant -case class RegisteredClaims( +final case class RegisteredClaims( iss: Option[String] = None, sub: Option[String] = None, aud: Seq[String] = Seq.empty, @@ -12,5 +12,6 @@ case class RegisteredClaims( jti: Option[String] = None, ) -object RegisteredClaims: +object RegisteredClaims { def empty: RegisteredClaims = RegisteredClaims() +} diff --git a/modules/oath-core/src/main/scala/io/oath/config/AlgorithmLoader.scala b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala similarity index 92% rename from modules/oath-core/src/main/scala/io/oath/config/AlgorithmLoader.scala rename to oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala index 2eff4ce..9e9e106 100644 --- a/modules/oath-core/src/main/scala/io/oath/config/AlgorithmLoader.scala +++ b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala @@ -11,12 +11,12 @@ import java.security.{KeyFactory, PrivateKey, PublicKey} import scala.util.Using import scala.util.chaining.* -object AlgorithmLoader: - private val SecretKeyConfigValue = "secret-key" - private val PrivateKeyPemPathConfigValue = "private-key-pem-path" - private val PublicKeyPemPathConfigValue = "public-key-pem-path" - private val RSAKeyFactoryInstance = "RSA" - private val ECKeyFactoryInstance = "EC" +object AlgorithmLoader { + inline private val SecretKeyConfigValue = "secret-key" + inline private val PrivateKeyPemPathConfigValue = "private-key-pem-path" + inline private val PublicKeyPemPathConfigValue = "public-key-pem-path" + inline private val RSAKeyFactoryInstance = "RSA" + inline private val ECKeyFactoryInstance = "EC" private val RSAKeyFactory = KeyFactory.getInstance(RSAKeyFactoryInstance) private val ECKeyFactory = KeyFactory.getInstance(ECKeyFactoryInstance) @@ -82,7 +82,7 @@ object AlgorithmLoader: private[oath] def loadOrThrow(algorithmScoped: Config, isIssuer: Boolean): Algorithm = val algorithm = algorithmScoped.getString("name") - algorithm.trim.toUpperCase match + algorithm.trim.toUpperCase match { case "HS256" => loadSecretKeyOrThrow(algorithmScoped).pipe(Algorithm.HMAC256) case "HS384" => @@ -110,3 +110,5 @@ object AlgorithmLoader: case "NONE" => Algorithm.none() case _ => throw new IllegalArgumentException(s"Unsupported signature algorithm: $algorithm") + } +} diff --git a/modules/oath-core/src/main/scala/io/oath/config/EncryptConfig.scala b/oath/core/src/main/scala/io/oath/config/EncryptConfig.scala similarity index 95% rename from modules/oath-core/src/main/scala/io/oath/config/EncryptConfig.scala rename to oath/core/src/main/scala/io/oath/config/EncryptConfig.scala index f672ce7..4dc1f0a 100644 --- a/modules/oath-core/src/main/scala/io/oath/config/EncryptConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/EncryptConfig.scala @@ -6,7 +6,7 @@ import scala.util.chaining.* private[oath] final case class EncryptConfig(secret: String) -object EncryptConfig: +object EncryptConfig { private def loadOrThrowEncryptConfig(encryptScoped: Config): EncryptConfig = encryptScoped .getMaybeNonEmptyString("secret") @@ -14,3 +14,4 @@ object EncryptConfig: .pipe(EncryptConfig.apply) private[config] def loadOrThrow(encryptScoped: Config): EncryptConfig = loadOrThrowEncryptConfig(encryptScoped) +} diff --git a/modules/oath-core/src/main/scala/io/oath/config/JwtIssuerConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala similarity index 86% rename from modules/oath-core/src/main/scala/io/oath/config/JwtIssuerConfig.scala rename to oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala index ff44141..8546d8b 100644 --- a/modules/oath-core/src/main/scala/io/oath/config/JwtIssuerConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala @@ -6,10 +6,15 @@ import io.oath.config.JwtIssuerConfig.RegisteredConfig import scala.concurrent.duration.FiniteDuration -case class JwtIssuerConfig(algorithm: Algorithm, encrypt: Option[EncryptConfig], registered: RegisteredConfig) +final case class JwtIssuerConfig(algorithm: Algorithm, encrypt: Option[EncryptConfig], registered: RegisteredConfig) -object JwtIssuerConfig: - case class RegisteredConfig( +object JwtIssuerConfig { + inline private val IssuerConfigLocation = "issuer" + inline private val AlgorithmConfigLocation = "algorithm" + inline private val EncryptConfigLocation = "encrypt" + inline private val RegisteredConfigLocation = "registered" + + final case class RegisteredConfig( issuerClaim: Option[String] = None, subjectClaim: Option[String] = None, audienceClaims: Seq[String] = Seq.empty, @@ -19,11 +24,6 @@ object JwtIssuerConfig: notBeforeOffset: Option[FiniteDuration] = None, ) - private val IssuerConfigLocation = "issuer" - private val AlgorithmConfigLocation = "algorithm" - private val EncryptConfigLocation = "encrypt" - private val RegisteredConfigLocation = "registered" - private def loadOrThrowRegisteredConfig(registeredScoped: Config): RegisteredConfig = val issuerClaim = registeredScoped.getMaybeNonEmptyString("issuer-claim") val subjectClaim = registeredScoped.getMaybeNonEmptyString("subject-claim") @@ -66,4 +66,4 @@ object JwtIssuerConfig: private[oath] def loadOrThrowOath(location: String): JwtIssuerConfig = JwtIssuerConfig.loadOrThrow(rootConfig.getConfig(location)) -end JwtIssuerConfig +} diff --git a/modules/oath-core/src/main/scala/io/oath/config/JwtManagerConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtManagerConfig.scala similarity index 84% rename from modules/oath-core/src/main/scala/io/oath/config/JwtManagerConfig.scala rename to oath/core/src/main/scala/io/oath/config/JwtManagerConfig.scala index 11dfcb2..7b3fffd 100644 --- a/modules/oath-core/src/main/scala/io/oath/config/JwtManagerConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtManagerConfig.scala @@ -4,9 +4,9 @@ import com.typesafe.config.{Config, ConfigFactory} import scala.util.chaining.scalaUtilChainingOps -case class JwtManagerConfig(issuer: JwtIssuerConfig, verifier: JwtVerifierConfig) +final case class JwtManagerConfig(issuer: JwtIssuerConfig, verifier: JwtVerifierConfig) -object JwtManagerConfig: +object JwtManagerConfig { private[oath] def loadOrThrowOath(location: String): JwtManagerConfig = JwtManagerConfig.loadOrThrow(rootConfig.getConfig(location)) @@ -17,3 +17,4 @@ object JwtManagerConfig: def loadOrThrow(config: Config): JwtManagerConfig = JwtManagerConfig(JwtIssuerConfig.loadOrThrow(config), JwtVerifierConfig.loadOrThrow(config)) +} diff --git a/modules/oath-core/src/main/scala/io/oath/config/JwtVerifierConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala similarity index 86% rename from modules/oath-core/src/main/scala/io/oath/config/JwtVerifierConfig.scala rename to oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala index 0623fa7..2a1058b 100644 --- a/modules/oath-core/src/main/scala/io/oath/config/JwtVerifierConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala @@ -3,37 +3,37 @@ package io.oath.config import com.auth0.jwt.algorithms.Algorithm import com.typesafe.config.{Config, ConfigFactory} import io.oath.config.EncryptConfig.* -import io.oath.config.JwtVerifierConfig._ +import io.oath.config.JwtVerifierConfig.* import scala.concurrent.duration.FiniteDuration -case class JwtVerifierConfig( +final case class JwtVerifierConfig( algorithm: Algorithm, encrypt: Option[EncryptConfig], providedWith: ProvidedWithConfig, leewayWindow: LeewayWindowConfig, ) -object JwtVerifierConfig: - case class ProvidedWithConfig( +object JwtVerifierConfig { + inline private val VerifierConfigLocation = "verifier" + inline private val AlgorithmConfigLocation = "algorithm" + inline private val EncryptConfigLocation = "encrypt" + inline private val ProvidedWithConfigLocation = "provided-with" + inline private val LeewayWindowConfigLocation = "leeway-window" + + final case class ProvidedWithConfig( issuerClaim: Option[String] = None, subjectClaim: Option[String] = None, audienceClaims: Seq[String] = Seq.empty, ) - case class LeewayWindowConfig( + final case class LeewayWindowConfig( leeway: Option[FiniteDuration] = None, issuedAt: Option[FiniteDuration] = None, expiresAt: Option[FiniteDuration] = None, notBefore: Option[FiniteDuration] = None, ) - private val VerifierConfigLocation = "verifier" - private val AlgorithmConfigLocation = "algorithm" - private val EncryptConfigLocation = "encrypt" - private val ProvidedWithConfigLocation = "provided-with" - private val LeewayWindowConfigLocation = "leeway-window" - private def loadOrdThrowProvidedWithConfig(providedWithScoped: Config): ProvidedWithConfig = val issuerClaim = providedWithScoped.getMaybeNonEmptyString("issuer-claim") val subjectClaim = providedWithScoped.getMaybeNonEmptyString("subject-claim") @@ -79,5 +79,4 @@ object JwtVerifierConfig: def loadOrThrow(location: String): JwtVerifierConfig = val configLocation = ConfigFactory.load().getConfig(location) loadOrThrow(configLocation) - -end JwtVerifierConfig +} diff --git a/oath/core/src/main/scala/io/oath/config/package.scala b/oath/core/src/main/scala/io/oath/config/package.scala new file mode 100644 index 0000000..88a1391 --- /dev/null +++ b/oath/core/src/main/scala/io/oath/config/package.scala @@ -0,0 +1,55 @@ +package io.oath.config + +import com.typesafe.config.{Config, ConfigException, ConfigFactory} + +import scala.concurrent.duration.FiniteDuration +import scala.jdk.CollectionConverters.* +import scala.jdk.DurationConverters.* +import scala.util.chaining.* +import scala.util.control.Exception.allCatch + +inline private[config] val OathLocation = "oath" +private[config] val rootConfig = ConfigFactory.load().getConfig(OathLocation) + +extension (config: Config) { + private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { case _: ConfigException.Missing => + default + } + + private[config] def getMaybeNonEmptyString(path: String): Option[String] = + allCatch + .withTry(Some(config.getString(path))) + .recover(ifMissingDefault(Option.empty)) + .toOption + .flatten + .tap(value => if (value.exists(_.isEmpty)) throw new IllegalArgumentException(s"$path empty string not allowed.")) + + private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = + allCatch + .withTry(Some(config.getDuration(path).toScala)) + .recover(ifMissingDefault(None)) + .get + + private[config] def getBooleanDefaultFalse(path: String): Boolean = + allCatch + .withTry(config.getBoolean(path)) + .recover(ifMissingDefault(false)) + .get + + private[config] def getSeqNonEmptyString(path: String): Seq[String] = + allCatch + .withTry(config.getStringList(path).asScala.toSeq) + .recover(ifMissingDefault(Seq.empty)) + .get + .tap(value => + if value.exists(_.isEmpty) then + throw new IllegalArgumentException(s"$path empty string in the list not allowed.") + ) + + private[config] def getMaybeConfig(path: String): Option[Config] = + allCatch + .withTry(Some(config.getConfig(path))) + .recover(ifMissingDefault(Option.empty)) + .toOption + .flatten +} diff --git a/modules/oath-core/src/main/scala/io/oath/json/ClaimsCodec.scala b/oath/core/src/main/scala/io/oath/json/ClaimsCodec.scala similarity index 100% rename from modules/oath-core/src/main/scala/io/oath/json/ClaimsCodec.scala rename to oath/core/src/main/scala/io/oath/json/ClaimsCodec.scala diff --git a/modules/oath-core/src/main/scala/io/oath/json/ClaimsDecoder.scala b/oath/core/src/main/scala/io/oath/json/ClaimsDecoder.scala similarity index 100% rename from modules/oath-core/src/main/scala/io/oath/json/ClaimsDecoder.scala rename to oath/core/src/main/scala/io/oath/json/ClaimsDecoder.scala diff --git a/modules/oath-core/src/main/scala/io/oath/json/ClaimsEncoder.scala b/oath/core/src/main/scala/io/oath/json/ClaimsEncoder.scala similarity index 100% rename from modules/oath-core/src/main/scala/io/oath/json/ClaimsEncoder.scala rename to oath/core/src/main/scala/io/oath/json/ClaimsEncoder.scala diff --git a/oath/core/src/main/scala/io/oath/package.scala b/oath/core/src/main/scala/io/oath/package.scala new file mode 100644 index 0000000..58a84a7 --- /dev/null +++ b/oath/core/src/main/scala/io/oath/package.scala @@ -0,0 +1,14 @@ +package io.oath + +import io.oath.utils.* + +// Type aliases with extra information, useful to determine the token type. +type JIssuer[_] = JwtIssuer +type JManager[_] = JwtManager +type JVerifier[_] = JwtVerifier + +inline private def getEnumValues[A]: Set[(A, String)] = + OathEnumMacro + .enumValues[A] + .toSet + .map(value => value -> convertUpperCamelToLowerHyphen(value.toString)) diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala new file mode 100644 index 0000000..4d4914e --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala @@ -0,0 +1,14 @@ +package io.oath.syntax + +import io.oath.JwtClaims + +trait JwtClaimsOps { + extension [A](value: A) + inline def toClaimsP: JwtClaims.ClaimsP[A] = JwtClaims.ClaimsP(value) + inline def toClaimsH: JwtClaims.ClaimsH[A] = JwtClaims.ClaimsH(value) + + extension [A, B](value: (A, B)) + inline def toClaimsHP: JwtClaims.ClaimsHP[A, B] = JwtClaims.ClaimsHP(value._1, value._2) +} + +object JwtClaimsOps extends JwtClaimsOps diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala new file mode 100644 index 0000000..87c68b0 --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala @@ -0,0 +1,14 @@ +package io.oath.syntax + +import io.oath.JwtToken + +trait JwtTokenOps { + extension (value: String) { + inline def toToken: JwtToken.Token = JwtToken.Token(value) + inline def toTokenH: JwtToken.TokenH = JwtToken.TokenH(value) + inline def toTokenP: JwtToken.TokenP = JwtToken.TokenP(value) + inline def toTokenHP: JwtToken.TokenHP = JwtToken.TokenHP(value) + } +} + +object JwtTokenOps extends JwtTokenOps diff --git a/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala b/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala new file mode 100644 index 0000000..ac2d604 --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala @@ -0,0 +1,9 @@ +package io.oath.syntax + +import io.oath.{JwtClaims, RegisteredClaims} + +trait RegisteredClaimsOps { + extension (value: RegisteredClaims) inline def toClaims: JwtClaims.Claims = JwtClaims.Claims(value) +} + +object RegisteredClaimsOps extends RegisteredClaimsOps diff --git a/oath/core/src/main/scala/io/oath/syntax/all.scala b/oath/core/src/main/scala/io/oath/syntax/all.scala new file mode 100644 index 0000000..a293ccd --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/all.scala @@ -0,0 +1,3 @@ +package io.oath.syntax + +object all extends JwtClaimsOps, JwtTokenOps, RegisteredClaimsOps diff --git a/modules/oath-core/src/main/scala/io/oath/utils/DecryptionUtils.scala b/oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala similarity index 90% rename from modules/oath-core/src/main/scala/io/oath/utils/DecryptionUtils.scala rename to oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala index d6fa0fd..80911be 100644 --- a/modules/oath-core/src/main/scala/io/oath/utils/DecryptionUtils.scala +++ b/oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala @@ -7,9 +7,9 @@ import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec import scala.util.control.Exception.allCatch -private[oath] object DecryptionUtils: +private[oath] object DecryptionUtils { - private def hexStringToByte(hexString: String): Array[Byte] = + private def hexStringToByte(hexString: String): Array[Byte] = { // https://stackoverflow.com/questions/140131/convert-a-string-representation-of-a-hex-dump-to-a-byte-array-using-java val len = hexString.length val data = new Array[Byte](len / 2) @@ -19,6 +19,7 @@ private[oath] object DecryptionUtils: Character.digit(hexString.charAt(i + 1), 16)).toByte i += 2 data + } def decryptAES(message: String, secret: String): Either[DecryptionError, String] = allCatch.withTry { @@ -28,3 +29,4 @@ private[oath] object DecryptionUtils: cipher.init(Cipher.DECRYPT_MODE, secretKeySpec) new String(cipher.doFinal(hexStringToByte(message))) }.toEither.left.map(e => JwtVerifyError.DecryptionError(e.getMessage)) +} diff --git a/modules/oath-core/src/main/scala/io/oath/utils/EncryptionUtils.scala b/oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala similarity index 96% rename from modules/oath-core/src/main/scala/io/oath/utils/EncryptionUtils.scala rename to oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala index 40e434b..b6cfc6e 100644 --- a/modules/oath-core/src/main/scala/io/oath/utils/EncryptionUtils.scala +++ b/oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala @@ -6,7 +6,7 @@ import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec import scala.util.control.Exception.allCatch -private[oath] object EncryptionUtils: +private[oath] object EncryptionUtils { private lazy val HexArray = "0123456789ABCDEF".toCharArray private def toHexString(bytes: Array[Byte]): String = @@ -29,3 +29,4 @@ private[oath] object EncryptionUtils: cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec) toHexString(cipher.doFinal(message.getBytes(UTF8))) }.toEither.left.map(e => JwtIssueError.EncryptionError(e.getMessage)) +} diff --git a/oath/core/src/main/scala/io/oath/utils/package.scala b/oath/core/src/main/scala/io/oath/utils/package.scala new file mode 100644 index 0000000..dfaf0aa --- /dev/null +++ b/oath/core/src/main/scala/io/oath/utils/package.scala @@ -0,0 +1,64 @@ +package io.oath.utils + +import com.auth0.jwt.JWTCreator.Builder +import com.auth0.jwt.interfaces.DecodedJWT +import io.oath.* +import io.oath.json.ClaimsEncoder + +import java.nio.charset.StandardCharsets +import java.time.Instant +import java.util.Base64 +import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.util.chaining.scalaUtilChainingOps +import scala.util.control.Exception.allCatch + +inline private[utils] val AES: "AES" = "AES" +inline private[utils] val UTF8: "utf-8" = "utf-8" + +private[oath] def convertUpperCamelToLowerHyphen(str: String): String = + str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim + +private[oath] def base64DecodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = + allCatch + .withTry(new String(Base64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) + .toEither + .left + .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) + +// TODO: report bug extension methods declared private in package oath are not visible +extension (decodedJWT: DecodedJWT) { + inline def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) + inline def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) + inline def getSeqAudience: Seq[String] = + Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten + inline def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) + inline def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) + inline def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) + inline def getOptionJwtID: Option[String] = Option(decodedJWT.getId) +} + +extension (builder: Builder) { + private def safeEncode[T]( + claims: T, + toBuilder: String => Builder, + )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = + allCatch + .withTry( + claimsEncoder + .encode(claims) + .pipe(toBuilder) + ) + .toEither + .left + .map(error => JwtIssueError.EncodeError(error.getMessage)) + + inline private[oath] def safeEncodeHeader[H](claims: H)(using + ClaimsEncoder[H] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withHeader) + + inline private[oath] def safeEncodePayload[P](claims: P)(using + ClaimsEncoder[P] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withPayload) +} diff --git a/modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala b/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala similarity index 100% rename from modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala rename to oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala diff --git a/modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala b/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala similarity index 100% rename from modules/oath-jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala rename to oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala diff --git a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala similarity index 100% rename from modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala rename to oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala diff --git a/modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala similarity index 100% rename from modules/oath-jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala rename to oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala diff --git a/modules/oath-macros/src/main/scala/io/oath/OathEnumMacro.scala b/oath/macros/src/main/scala/io/oath/OathEnumMacro.scala similarity index 100% rename from modules/oath-macros/src/main/scala/io/oath/OathEnumMacro.scala rename to oath/macros/src/main/scala/io/oath/OathEnumMacro.scala diff --git a/modules/oath-macros/src/test/scala/io/oath/OathEnum.scala b/oath/macros/src/test/scala/io/oath/OathEnum.scala similarity index 100% rename from modules/oath-macros/src/test/scala/io/oath/OathEnum.scala rename to oath/macros/src/test/scala/io/oath/OathEnum.scala diff --git a/modules/oath-macros/src/test/scala/io/oath/OathEnumMacroSpec.scala b/oath/macros/src/test/scala/io/oath/OathEnumMacroSpec.scala similarity index 100% rename from modules/oath-macros/src/test/scala/io/oath/OathEnumMacroSpec.scala rename to oath/macros/src/test/scala/io/oath/OathEnumMacroSpec.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 935df21..72dcc01 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,65 +1,32 @@ import sbt.* -import sbt.Keys.* object Dependencies { - object Versions { - val scalaTest = "3.2.19" - val scalaTestPlusCheck = "3.2.17.0" - val scalacheck = "1.17.1" - val javaJWT = "4.4.0" - val config = "1.4.3" - val bcprov = "1.78.1" - val circe = "0.14.7" - val jsoniterScala = "2.27.3" - } - - object Testing { - val scalaTest = "org.scalatest" %% "scalatest" % Versions.scalaTest % Test - val scalaTestPlusCheck = "org.scalatestplus" %% "scalacheck-1-17" % Versions.scalaTestPlusCheck % Test - val scalacheck = "org.scalacheck" %% "scalacheck" % Versions.scalacheck % Test - - val all = Seq(scalaTest, scalaTestPlusCheck, scalacheck) - } - - object Circe { - val core = "io.circe" %% "circe-core" % Versions.circe - val generic = "io.circe" %% "circe-generic" % Versions.circe - val parser = "io.circe" %% "circe-parser" % Versions.circe - - val all = Seq(core, generic, parser) - } - - object JsoniterScala { - val core = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % Versions.jsoniterScala - val macros = - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % Versions.jsoniterScala % "provided" - - val all = Seq(core, macros) - } - - object Utils { - val config = "com.typesafe" % "config" % Versions.config - val bcprov = "org.bouncycastle" % "bcprov-jdk18on" % Versions.bcprov - - val all = Seq(config, bcprov) - } - - object Auth0 { - val javaJWT = "com.auth0" % "java-jwt" % Versions.javaJWT - - val all = Seq(javaJWT) - } - - lazy val oathMacros = - libraryDependencies ++= Testing.all - - lazy val oathCore = - libraryDependencies ++= Testing.all ++ Auth0.all ++ Utils.all ++ Circe.all.map(_ % Test) - - lazy val oathCirce = - libraryDependencies ++= Circe.all - - lazy val oathJsoniterScala = - libraryDependencies ++= JsoniterScala.all + val scalaTestV = "3.2.19" + val scalaTestPlusCheckV = "3.2.17.0" + val scalacheckV = "1.17.1" + val javaJWTV = "4.4.0" + val configV = "1.4.3" + val bcprovV = "1.78.1" + val circeV = "0.14.7" + val jsoniterScalaV = "2.27.3" + val catsV = "2.12.0" + + val scalaTest = "org.scalatest" %% "scalatest" % scalaTestV + val scalaTestPlusScalaCheck = "org.scalatestplus" %% "scalacheck-1-17" % scalaTestPlusCheckV + val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheckV + + val circeCore = "io.circe" %% "circe-core" % circeV + val circeGeneric = "io.circe" %% "circe-generic" % circeV + val circeParser = "io.circe" %% "circe-parser" % circeV + + val jsoniterScalacore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterScalaV + val jsoniterScalamacros = + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterScalaV % "provided" + + val cats = "org.typelevel" %% "cats-core" % catsV + val typesafeConfig = "com.typesafe" % "config" % configV + val bcprov = "org.bouncycastle" % "bcprov-jdk18on" % bcprovV + + val javaJWT = "com.auth0" % "java-jwt" % javaJWTV } diff --git a/project/Projects.scala b/project/Projects.scala index fb5ad28..d6f88a4 100644 --- a/project/Projects.scala +++ b/project/Projects.scala @@ -3,8 +3,4 @@ import sbt.* import sbt.Keys.* import scalafix.sbt.ScalafixPlugin.autoImport.scalafixOnCompile -object Projects { - - - -} +object Projects {} From 80048897447b25ca98f4f358bb3b3de2a78cf4c9 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Wed, 28 Aug 2024 09:55:33 +0100 Subject: [PATCH 05/15] feat: improve codebase --- build.sbt | 85 ++++---- .../main/scala/io/oath/test/Arbitraries.scala | 4 +- .../main/scala/io/oath/test/CodecUtils.scala | 2 +- .../src/main/scala/io/oath/test/Main.scala | 56 ++++++ oath/core-test/src/test/resources/issuer.conf | 21 -- .../src/test/resources/verifier.conf | 27 +-- .../test/scala/io/oath/JwtIssuerSpec.scala | 5 +- .../test/scala/io/oath/JwtVerifierSpec.scala | 25 ++- .../scala/io/oath/{utils => }/UtilsSpec.scala | 3 +- .../io/oath/config/JwtIssuerLoaderSpec.scala | 16 -- .../oath/config/JwtVerifierLoaderSpec.scala | 16 -- .../main/scala/io/oath/JwtIssueError.scala | 12 +- .../src/main/scala/io/oath/JwtIssuer.scala | 31 +-- .../src/main/scala/io/oath/JwtToken.scala | 14 +- .../src/main/scala/io/oath/JwtVerifier.scala | 57 ++---- .../main/scala/io/oath/JwtVerifyError.scala | 18 +- .../scala/io/oath/config/EncryptConfig.scala | 17 -- .../io/oath/config/JwtIssuerConfig.scala | 11 +- .../io/oath/config/JwtVerifierConfig.scala | 8 +- .../main/scala/io/oath/config/package.scala | 15 +- .../core/src/main/scala/io/oath/package.scala | 60 +++++- .../scala/io/oath/utils/DecryptionUtils.scala | 32 --- .../scala/io/oath/utils/EncryptionUtils.scala | 32 --- .../main/scala/io/oath/utils/package.scala | 64 ------ .../src/test/resources/algorithm-es256.conf | 5 + .../src/test/resources/algorithm-es384.conf | 5 + .../src/test/resources/algorithm-es512.conf | 5 + .../src/test/resources/algorithm-hsxxx.conf | 4 + .../src/test/resources/algorithm-none.conf | 3 + .../src/test/resources/algorithm-rsxxx.conf | 5 + .../test/resources/algorithm-unsupported.conf | 3 + oath/core/src/test/resources/issuer.conf | 66 ++++++ oath/core/src/test/resources/manager.conf | 31 +++ oath/core/src/test/resources/reference.conf | 113 +++++++++++ oath/core/src/test/resources/verifier.conf | 71 +++++++ .../test/scala/io/oath/JwtIssuerSpec.scala | 189 ++++++++++++++++++ .../src/test/scala/io/oath/NestedHeader.scala | 37 ++++ .../test/scala/io/oath/NestedPayload.scala | 37 ++++ .../src/test/scala/io/oath/OathToken.scala | 5 + .../src/test/scala/io/oath/UtilsSpec.scala | 29 +++ .../io/oath/config/AlgorithmLoaderSpec.scala | 101 ++++++++++ .../io/oath/config/JwtIssuerLoaderSpec.scala | 77 +++++++ .../io/oath/config/JwtManagerLoaderSpec.scala | 37 ++++ .../oath/config/JwtVerifierLoaderSpec.scala | 78 ++++++++ .../io/oath/testkit/AnyWordSpecBase.scala | 7 + .../scala/io/oath/testkit/Arbitraries.scala | 141 +++++++++++++ .../scala/io/oath/testkit/ClockHelper.scala | 10 + .../oath/testkit/PropertyBasedTesting.scala | 11 + oath/core/src/test/secrets/es256-private.pem | 5 + oath/core/src/test/secrets/es256-public.pem | 4 + oath/core/src/test/secrets/es384-private.pem | 6 + oath/core/src/test/secrets/es384-public.pem | 5 + oath/core/src/test/secrets/es512-private.pem | 7 + oath/core/src/test/secrets/es512-public.pem | 6 + oath/core/src/test/secrets/rsa-private.pem | 28 +++ oath/core/src/test/secrets/rsa-public.pem | 9 + .../JsoniterConversionSpec.scala | 3 +- project/Dependencies.scala | 44 ++-- project/Projects.scala | 6 - project/build.properties | 2 +- 60 files changed, 1393 insertions(+), 433 deletions(-) create mode 100644 oath/core-test/src/main/scala/io/oath/test/Main.scala rename oath/core-test/src/test/scala/io/oath/{utils => }/UtilsSpec.scala (94%) delete mode 100644 oath/core/src/main/scala/io/oath/config/EncryptConfig.scala delete mode 100644 oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala delete mode 100644 oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala delete mode 100644 oath/core/src/main/scala/io/oath/utils/package.scala create mode 100644 oath/core/src/test/resources/algorithm-es256.conf create mode 100644 oath/core/src/test/resources/algorithm-es384.conf create mode 100644 oath/core/src/test/resources/algorithm-es512.conf create mode 100644 oath/core/src/test/resources/algorithm-hsxxx.conf create mode 100644 oath/core/src/test/resources/algorithm-none.conf create mode 100644 oath/core/src/test/resources/algorithm-rsxxx.conf create mode 100644 oath/core/src/test/resources/algorithm-unsupported.conf create mode 100644 oath/core/src/test/resources/issuer.conf create mode 100644 oath/core/src/test/resources/manager.conf create mode 100644 oath/core/src/test/resources/reference.conf create mode 100644 oath/core/src/test/resources/verifier.conf create mode 100644 oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/NestedHeader.scala create mode 100644 oath/core/src/test/scala/io/oath/NestedPayload.scala create mode 100644 oath/core/src/test/scala/io/oath/OathToken.scala create mode 100644 oath/core/src/test/scala/io/oath/UtilsSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala create mode 100644 oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala create mode 100644 oath/core/src/test/scala/io/oath/testkit/ClockHelper.scala create mode 100644 oath/core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala create mode 100644 oath/core/src/test/secrets/es256-private.pem create mode 100644 oath/core/src/test/secrets/es256-public.pem create mode 100644 oath/core/src/test/secrets/es384-private.pem create mode 100644 oath/core/src/test/secrets/es384-public.pem create mode 100644 oath/core/src/test/secrets/es512-private.pem create mode 100644 oath/core/src/test/secrets/es512-public.pem create mode 100644 oath/core/src/test/secrets/rsa-private.pem create mode 100644 oath/core/src/test/secrets/rsa-public.pem delete mode 100644 project/Projects.scala diff --git a/build.sbt b/build.sbt index b808ab1..4a28ef4 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,8 @@ -import Dependencies.* import org.typelevel.sbt.gha.Permissions -import scala.util.chaining.* - Global / onChangedBuildSource := ReloadOnSourceChanges -ThisBuild / scalaVersion := "3.4.1" +ThisBuild / scalaVersion := "3.4.2" ThisBuild / organization := "io.github.scala-jwt" ThisBuild / organizationName := "oath" ThisBuild / organizationHomepage := Some(url("https://github.com/scala-jwt/oath")) @@ -55,81 +52,99 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( ThisBuild / Test / fork := true ThisBuild / run / fork := true ThisBuild / Test / parallelExecution := true +ThisBuild / Test / testForkedParallel := true ThisBuild / scalafmtOnCompile := sys.env.getOrElse("RUN_SCALAFMT_ON_COMPILE", "false").toBoolean ThisBuild / scalafixOnCompile := sys.env.getOrElse("RUN_SCALAFIX_ON_COMPILE", "false").toBoolean ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := "4.8.15" -lazy val rootModuleName = "root" - -def rootModule(rootModule: String)(subModule: String): Project = - Project(s"$rootModule-$subModule", file(s"$rootModule${if (subModule == rootModuleName) "" else s"/$subModule"}")) - .pipe(project => - if (subModule == rootModuleName) project.enablePlugins(NoPublishPlugin) - else project - ) +def rootModule(rootModule: String)(subModule: Option[String]): Project = + Project( + s"$rootModule${subModule.map("-" + _).getOrElse("")}", + file(s"$rootModule${subModule.map("/" + _).getOrElse("")}"), + ) -lazy val root = Project("oath", file(".")) +lazy val root = Project("oath-root", file(".")) .enablePlugins(NoPublishPlugin) .settings(Aliases.all) .aggregate(allModules *) lazy val example = project .in(file("example")) + .enablePlugins(NoPublishPlugin) .dependsOn(oathCore, oathCirce, oathJsoniterScala) -val createOathModule = rootModule("oath") _ +lazy val createOathModule = rootModule("oath") _ -lazy val oathRoot = createOathModule("root") +lazy val oathRoot = createOathModule(None) + .enablePlugins(NoPublishPlugin) .aggregate(oathModules *) -lazy val oathMacros = createOathModule("macros") +lazy val oathMacros = createOathModule(Some("macros")) .settings( libraryDependencies ++= Seq( - scalaTest % Test, - scalaTestPlusScalaCheck % Test, - scalacheck % Test, + Dependencies.scalaTest % Test, + Dependencies.scalaTestPlusScalaCheck % Test, + Dependencies.scalacheck % Test, ) ) -lazy val oathCore = createOathModule("core") +lazy val oathCore = createOathModule(Some("core")) .dependsOn(oathMacros) .settings( libraryDependencies ++= Seq( - javaJWT, - typesafeConfig, - bcprov, - cats, + Dependencies.javaJWT, + Dependencies.typesafeConfig, + Dependencies.bcprov, + Dependencies.cats, + Dependencies.tink, + Dependencies.scalaTest % Test, + Dependencies.scalaTestPlusScalaCheck % Test, + Dependencies.scalacheck % Test, + Dependencies.circeCore % Test, + Dependencies.circeGeneric % Test, + Dependencies.circeParser % Test, ) ) -lazy val oathCoreTest = createOathModule("core-test") +lazy val oathCoreTest = createOathModule(Some("core-test")) .enablePlugins(NoPublishPlugin) .dependsOn(oathCore) .settings( libraryDependencies ++= Seq( - scalaTest, - scalaTestPlusScalaCheck, - scalacheck, - circeCore, - circeGeneric, - circeParser, + Dependencies.scalaTest, + Dependencies.scalaTestPlusScalaCheck, + Dependencies.scalacheck, + Dependencies.circeCore, + Dependencies.circeGeneric, + Dependencies.circeParser, ) ) -lazy val oathCirce = createOathModule("circe") +lazy val oathCirce = createOathModule(Some("circe")) .dependsOn( oathCore, oathCoreTest % Test, ) - .settings(libraryDependencies ++= Seq(circeCore, circeGeneric, circeParser)) + .settings( + libraryDependencies ++= Seq( + Dependencies.circeCore, + Dependencies.circeGeneric, + Dependencies.circeParser, + ) + ) -lazy val oathJsoniterScala = createOathModule("jsoniter-scala") +lazy val oathJsoniterScala = createOathModule(Some("jsoniter-scala")) .dependsOn( oathCore, oathCoreTest % Test, ) - .settings(libraryDependencies ++= Seq(jsoniterScalacore, jsoniterScalamacros)) + .settings( + libraryDependencies ++= Seq( + Dependencies.jsoniterScalacore, + Dependencies.jsoniterScalamacros, + ) + ) lazy val oathModules: Seq[ProjectReference] = Seq( oathMacros, diff --git a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala index d7c4c4f..ed7cc5f 100644 --- a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala +++ b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala @@ -13,8 +13,8 @@ import java.time.Instant import scala.concurrent.duration.{Duration, DurationInt} trait Arbitraries { - lazy val genPositiveFiniteDuration = Gen.posNum[Long].map(Duration.fromNanos) - lazy val genPositiveFiniteDurationSeconds = Gen.posNum[Int].map(x => (x + 1).seconds) + private lazy val genPositiveFiniteDuration = Gen.posNum[Long].map(Duration.fromNanos) + private lazy val genPositiveFiniteDurationSeconds = Gen.posNum[Int].map(x => (x + 1).seconds) implicit val arbNonEmptyString: Arbitrary[String] = Arbitrary( diff --git a/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala b/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala index 9bedf7f..60ff175 100644 --- a/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala +++ b/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala @@ -3,7 +3,7 @@ package io.oath.test import com.fasterxml.jackson.databind.ObjectMapper trait CodecUtils { - val mapper = new ObjectMapper + private val mapper = new ObjectMapper() def unsafeParseJsonToJavaMap(json: String): java.util.Map[String, Object] = mapper.readValue(json, classOf[java.util.HashMap[String, Object]]) diff --git a/oath/core-test/src/main/scala/io/oath/test/Main.scala b/oath/core-test/src/main/scala/io/oath/test/Main.scala new file mode 100644 index 0000000..d7ca55c --- /dev/null +++ b/oath/core-test/src/main/scala/io/oath/test/Main.scala @@ -0,0 +1,56 @@ +package io.oath.test + +import cats.syntax.all.* +import com.auth0.jwt.{JWT, JWTCreator} +import io.oath.config.{EncryptConfig, JwtVerifierConfig} +import io.oath.syntax.all.* +import io.oath.{JwtVerifier, RegisteredClaims} + +import java.time.Instant +import java.time.temporal.ChronoUnit +import scala.util.chaining.scalaUtilChainingOps + +object Main extends App, Arbitraries { + + def getInstantNowSeconds: Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) + + def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig) = { + val now = getInstantNowSeconds + val leeway = config.leewayWindow.leeway.map(leeway => now.plusSeconds(leeway.toSeconds - 1)) + val expiresAt = config.leewayWindow.expiresAt.map(expiresAt => now.plusSeconds(expiresAt.toSeconds - 1)) + val notBefore = config.leewayWindow.notBefore.map(notBefore => now.plusSeconds(notBefore.toSeconds - 1)) + val issueAt = config.leewayWindow.issuedAt.map(issueAt => now.plusSeconds(issueAt.toSeconds - 1)) + + val registeredClaims = RegisteredClaims( + config.providedWith.issuerClaim, + config.providedWith.subjectClaim, + config.providedWith.audienceClaims, + expiresAt orElse leeway, + notBefore orElse leeway, + issueAt orElse leeway, + None, + ) + + val builderWithRegistered = builder + .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) + .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) + .tap(builder => builder.withAudience(registeredClaims.aud*)) + .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) + .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) + .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) + + registeredClaims -> builderWithRegistered + } + + val defaultConfig = arbJwtVerifierConfig.arbitrary.sample.get + + val jwtVerifier = new JwtVerifier(defaultConfig.copy(encrypt = EncryptConfig("secret").some)) + + val (_, builder) = setRegisteredClaims(JWT.create(), defaultConfig) + + val token = builder.sign(defaultConfig.algorithm) + + val verified = jwtVerifier.verifyJwt(token.toToken) + + println(verified) +} diff --git a/oath/core-test/src/test/resources/issuer.conf b/oath/core-test/src/test/resources/issuer.conf index f8e2743..87e1b16 100644 --- a/oath/core-test/src/test/resources/issuer.conf +++ b/oath/core-test/src/test/resources/issuer.conf @@ -23,27 +23,6 @@ token { } } -token-with-encryption { - algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-private.pem" - } - encrypt { - secret = "password" - } - issuer { - registered { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = false - expires-at-offset = 1 day - not-before-offset = 1 minute - } - } -} - without-private-key-token { algorithm { name = "RS256" diff --git a/oath/core-test/src/test/resources/verifier.conf b/oath/core-test/src/test/resources/verifier.conf index 5a2a76f..b192465 100644 --- a/oath/core-test/src/test/resources/verifier.conf +++ b/oath/core-test/src/test/resources/verifier.conf @@ -25,37 +25,12 @@ token { } } -token-with-encryption { - algorithm { - name = "RS256" - public-key-pem-path = "src/test/secrets/rsa-public.pem" - } - encrypt { - secret = "password" - } - verifier { - provided-with { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - } - leeway-window { - leeway = 1 minute - issued-at = 4 minutes - expires-at = 3 minutes - not-before = 2 minutes - } - } -} - without-public-key-token { algorithm { name = "RS256" private-key-pem-path = "src/test/secrets/rsa-public.pem" } - encrypt { - key = "password" - } + verifier { provided-with { issuer-claim = "issuer" diff --git a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala index 129f4a8..4deea66 100644 --- a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -7,7 +7,6 @@ import io.oath.syntax.all.* import io.oath.test.NestedHeader.nestedHeaderDecoder import io.oath.test.NestedPayload.nestedPayloadDecoder import io.oath.test.* -import io.oath.utils.* import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.ListHasAsScala @@ -223,8 +222,8 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) val jwt = jwtIssuer.issueJwt() - jwt.left.value shouldEqual JwtIssueError.IllegalArgument( - "JwtIssuer failed with IllegalArgumentException", + jwt.left.value shouldEqual JwtIssueError.SignError( + "Signing token failed", new IllegalArgumentException("Algorithm cannot be null"), ) } diff --git a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala index 11e96fb..aa927ca 100644 --- a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -9,7 +9,6 @@ import io.oath.syntax.all.* import io.oath.test.NestedHeader.{SimpleHeader, nestedHeaderEncoder} import io.oath.test.NestedPayload.{SimplePayload, nestedPayloadEncoder} import io.oath.test.* -import io.oath.utils.* import scala.util.chaining.scalaUtilChainingOps @@ -242,7 +241,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimpleHeader](token.toTokenH) - verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) } "fail to decode a token with payload if exception raised in decoder" in { @@ -254,7 +253,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimplePayload](token.toTokenP) - verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) } "fail to decode a token with header & payload if exception raised in decoder" in { @@ -267,7 +266,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) - verified.left.value shouldEqual JwtVerifyError.UnexpectedError("Boom") + verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) } "fail to verify token with VerificationError when provided with claims are not meet criteria" in { @@ -296,9 +295,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldEqual JwtVerifyError.IllegalArgument( + verified.left.value shouldEqual JwtVerifyError.VerificationError( "JwtVerifier failed with IllegalArgumentException", - new IllegalArgumentException("The Algorithm cannot be null."), + Some(new IllegalArgumentException("The Algorithm cannot be null.")), ) } @@ -314,9 +313,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.left.value shouldEqual JwtVerifyError - .AlgorithmMismatch( - "JwtVerifier failed with AlgorithmMismatchException", - new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected."), + .VerificationError( + "JwtVerifier failed with verification error", + Some(new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected.")), ) } @@ -332,9 +331,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.left.value shouldEqual JwtVerifyError - .SignatureVerificationError( + .VerificationError( "JwtVerifier failed with SignatureVerificationException", - new SignatureVerificationException(algorithm), + null, ) } @@ -349,9 +348,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldBe + verified.left.value shouldEqual JwtVerifyError - .TokenExpired(s"The Token has expired on $expiresAt.") + .VerificationError(s"The Token has expired on $expiresAt.", null) } "fail to verify an empty string token" in { diff --git a/oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala b/oath/core-test/src/test/scala/io/oath/UtilsSpec.scala similarity index 94% rename from oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala rename to oath/core-test/src/test/scala/io/oath/UtilsSpec.scala index 99de717..afcdd0c 100644 --- a/oath/core-test/src/test/scala/io/oath/utils/UtilsSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/UtilsSpec.scala @@ -1,7 +1,6 @@ -package io.oath.utils +package io.oath import io.oath.test.* -import io.oath.utils.* class UtilsSpec extends AnyWordSpecBase { diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index a347540..a4fed60 100644 --- a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -10,7 +10,6 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase { inline val configFile = "issuer" inline val DefaultTokenConfigLocation = "default-token" inline val TokenConfigLocation = "token" - inline val TokenWithEncryptionConfigLocation = "token-with-encryption" inline val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" inline val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" inline val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" @@ -46,21 +45,6 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase { config.algorithm.getName shouldBe "RS256" } - "load token issuer config values from configuration file with encryption key" in { - val configLoader = ConfigFactory.load(configFile).getConfig(TokenWithEncryptionConfigLocation) - val config = JwtIssuerConfig.loadOrThrow(configLoader) - - config.encrypt shouldBe Some(EncryptConfig("password")) - config.registered.issuerClaim shouldBe Some("issuer") - config.registered.subjectClaim shouldBe Some("subject") - config.registered.audienceClaims shouldBe Seq("aud1", "aud2") - config.registered.includeIssueAtClaim shouldBe true - config.registered.includeJwtIdClaim shouldBe false - config.registered.expiresAtOffset shouldBe Some(1.day) - config.registered.notBeforeOffset shouldBe Some(1.minute) - config.algorithm.getName shouldBe "RS256" - } - "load token issuer config values from reference configuration file using location" in { val config = JwtIssuerConfig.loadOrThrow(TokenConfigLocation) diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala index 0330b73..4f3b091 100644 --- a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -10,7 +10,6 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase { val configFile = "verifier" val DefaultTokenConfigLocation = "default-token" val TokenConfigLocation = "token" - val TokenWithEncryptionConfigLocation = "token-with-encryption" val WithoutPublicKeyTokenConfigLocation = "without-public-key-token" val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" @@ -47,21 +46,6 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase { config.algorithm.getName shouldBe "RS256" } - "load token verifier config values from configuration file with encryption" in { - val configLoader = ConfigFactory.load(configFile).getConfig(TokenWithEncryptionConfigLocation) - val config = JwtVerifierConfig.loadOrThrow(configLoader) - - config.encrypt shouldBe Some(EncryptConfig("password")) - config.providedWith.issuerClaim shouldBe Some("issuer") - config.providedWith.subjectClaim shouldBe Some("subject") - config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") - config.leewayWindow.leeway shouldBe Some(1.minute) - config.leewayWindow.issuedAt shouldBe Some(4.minutes) - config.leewayWindow.expiresAt shouldBe Some(3.minutes) - config.leewayWindow.notBefore shouldBe Some(2.minutes) - config.algorithm.getName shouldBe "RS256" - } - "load token verifier config values from reference configuration file using location" in { val config = JwtVerifierConfig.loadOrThrow(TokenConfigLocation) diff --git a/oath/core/src/main/scala/io/oath/JwtIssueError.scala b/oath/core/src/main/scala/io/oath/JwtIssueError.scala index 529a60f..9960a83 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssueError.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssueError.scala @@ -6,15 +6,7 @@ sealed abstract class JwtIssueError(error: String, cause: Option[Throwable] = No extends Exception(error, cause.orNull) object JwtIssueError { - case class IllegalArgument(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) + final case class SignError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) - case class JwtCreationIssueError(message: String, underlying: Throwable) - extends JwtIssueError(message, underlying.some) - - case class EncryptionError(message: String) extends JwtIssueError(message) - - case class EncodeError(message: String) extends JwtIssueError(message) - - case class UnexpectedIssueError(message: String, underlying: Option[Throwable] = None) - extends JwtIssueError(message, underlying) + final case class EncodeError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) } diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index f321b69..ce37d03 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -1,12 +1,10 @@ package io.oath import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.JWTCreationException import com.auth0.jwt.{JWT, JWTCreator} import io.oath.* import io.oath.config.* import io.oath.json.ClaimsEncoder -import io.oath.utils.* import java.time.temporal.ChronoUnit import java.time.{Clock, Instant} @@ -49,29 +47,12 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) } - private def maybeEncryptJwt[T <: JwtClaims]( - jwt: Jwt[T] - ): Either[JwtIssueError.EncryptionError, Jwt[T]] = - config.encrypt - .map(encryptConfig => - EncryptionUtils - .encryptAES(jwt.token, encryptConfig.secret) - .map(encryptedToken => jwt.copy(token = encryptedToken)) - ) - .getOrElse(Right(jwt)) - private def safeSign(builder: JWTCreator.Builder, algorithm: Algorithm): Either[JwtIssueError, String] = allCatch .withTry(builder.sign(algorithm)) .toEither .left - .map { - case e: IllegalArgumentException => - JwtIssueError.IllegalArgument("JwtIssuer failed with IllegalArgumentException", e) - case e: JWTCreationException => - JwtIssueError.JwtCreationIssueError("JwtIssuer failed with JWTCreationException", e) - case e => JwtIssueError.UnexpectedIssueError("JwtIssuer failed with unexpected exception", Some(e)) - } + .map(e => JwtIssueError.SignError("Signing token failed", e)) def issueJwt( claims: JwtClaims.Claims = JwtClaims.Claims() @@ -88,7 +69,6 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) ) ) } - .flatMap(jwt => maybeEncryptJwt(jwt)) } def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using @@ -104,8 +84,7 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) claims.copy(registered = registeredClaims), token, ) - encryptedJwt <- maybeEncryptJwt(jwt) - yield encryptedJwt + yield jwt } def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using @@ -121,8 +100,7 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) claims.copy(registered = registeredClaims), token, ) - encryptedJwt <- maybeEncryptJwt(jwt) - yield encryptedJwt + yield jwt } def issueJwt[H, P]( @@ -139,7 +117,6 @@ final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) claims.copy(registered = registeredClaims), token, ) - encryptedJwt <- maybeEncryptJwt(jwt) - yield encryptedJwt + yield jwt } } diff --git a/oath/core/src/main/scala/io/oath/JwtToken.scala b/oath/core/src/main/scala/io/oath/JwtToken.scala index 3da9fb6..4399bb5 100644 --- a/oath/core/src/main/scala/io/oath/JwtToken.scala +++ b/oath/core/src/main/scala/io/oath/JwtToken.scala @@ -1,14 +1,10 @@ package io.oath -sealed trait JwtToken: - def token: String +sealed abstract class JwtToken(val token: String) object JwtToken { - case class Token(token: String) extends JwtToken - - case class TokenH(token: String) extends JwtToken - - case class TokenP(token: String) extends JwtToken - - case class TokenHP(token: String) extends JwtToken + final case class Token(override val token: String) extends JwtToken(token) + final case class TokenH(override val token: String) extends JwtToken(token) + final case class TokenP(override val token: String) extends JwtToken(token) + final case class TokenHP(override val token: String) extends JwtToken(token) } diff --git a/oath/core/src/main/scala/io/oath/JwtVerifier.scala b/oath/core/src/main/scala/io/oath/JwtVerifier.scala index 363cb58..50021bf 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifier.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifier.scala @@ -1,12 +1,10 @@ package io.oath import com.auth0.jwt.JWT -import com.auth0.jwt.exceptions.* import com.auth0.jwt.interfaces.DecodedJWT import io.oath.* import io.oath.config.JwtVerifierConfig import io.oath.json.* -import io.oath.utils.* import scala.util.chaining.scalaUtilChainingOps import scala.util.control.Exception.allCatch @@ -48,11 +46,6 @@ final class JwtVerifier(config: JwtVerifierConfig) { jti = decodedJWT.getOptionJwtID, ) - inline private def maybeDecryptJwt(token: String): Either[JwtVerifyError.DecryptionError, String] = - config.encrypt - .map(encryptionConfig => DecryptionUtils.decryptAES(token, encryptionConfig.secret)) - .getOrElse(Right(token)) - inline private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = Option(token) .filter(_.nonEmpty) @@ -70,25 +63,12 @@ final class JwtVerifier(config: JwtVerifierConfig) { .withTry(jwtVerifier.verify(token)) .toEither .left - .map { - case e: IllegalArgumentException => - JwtVerifyError.IllegalArgument("JwtVerifier failed with IllegalArgumentException", e) - case e: AlgorithmMismatchException => - JwtVerifyError.AlgorithmMismatch("JwtVerifier failed with AlgorithmMismatchException", e) - case e: SignatureVerificationException => - JwtVerifyError.SignatureVerificationError("JwtVerifier failed with SignatureVerificationException", e) - case e: TokenExpiredException => - JwtVerifyError.TokenExpired("JwtVerifier failed with TokenExpiredException", Some(e)) - case e: JWTVerificationException => - JwtVerifyError.VerificationError("JwtVerifier failed with JWTVerificationException", Some(e)) - case e => JwtVerifyError.UnexpectedError("JwtVerifier failed with unexpected exception", Some(e)) - } + .map(e => JwtVerifyError.VerificationError("JwtVerifier failed with verification error", Some(e))) def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = for - token <- validateToken(jwt.token) - decryptedToken <- maybeDecryptJwt(token) - decodedJwt <- verify(decryptedToken) + token <- validateToken(jwt.token) + decodedJwt <- verify(token) registeredClaims = getRegisteredClaims(decodedJwt) yield JwtClaims.Claims(registeredClaims) @@ -96,11 +76,10 @@ final class JwtVerifier(config: JwtVerifierConfig) { claimsDecoder: ClaimsDecoder[H] ): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] = for - token <- validateToken(jwt.token) - decryptedToken <- maybeDecryptJwt(token) - decodedJwt <- verify(decryptedToken) - json <- base64DecodeToken(decodedJwt.getHeader) - payload <- safeDecode(claimsDecoder.decode(json)) + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + json <- base64DecodeToken(decodedJwt.getHeader) + payload <- safeDecode(claimsDecoder.decode(json)) registeredClaims = getRegisteredClaims(decodedJwt) yield JwtClaims.ClaimsH(payload, registeredClaims) @@ -108,11 +87,10 @@ final class JwtVerifier(config: JwtVerifierConfig) { claimsDecoder: ClaimsDecoder[P] ): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] = for - token <- validateToken(jwt.token) - decryptedToken <- maybeDecryptJwt(token) - decodedJwt <- verify(decryptedToken) - json <- base64DecodeToken(decodedJwt.getPayload) - payload <- safeDecode(claimsDecoder.decode(json)) + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + json <- base64DecodeToken(decodedJwt.getPayload) + payload <- safeDecode(claimsDecoder.decode(json)) registeredClaims = getRegisteredClaims(decodedJwt) yield JwtClaims.ClaimsP(payload, registeredClaims) @@ -121,13 +99,12 @@ final class JwtVerifier(config: JwtVerifierConfig) { payloadDecoder: ClaimsDecoder[P], ): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = for - token <- validateToken(jwt.token) - decryptedToken <- maybeDecryptJwt(token) - decodedJwt <- verify(decryptedToken) - jsonHeader <- base64DecodeToken(decodedJwt.getHeader) - jsonPayload <- base64DecodeToken(decodedJwt.getPayload) - headerClaims <- safeDecode(headerDecoder.decode(jsonHeader)) - payloadClaims <- safeDecode(payloadDecoder.decode(jsonPayload)) + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + jsonHeader <- base64DecodeToken(decodedJwt.getHeader) + jsonPayload <- base64DecodeToken(decodedJwt.getPayload) + headerClaims <- safeDecode(headerDecoder.decode(jsonHeader)) + payloadClaims <- safeDecode(payloadDecoder.decode(jsonPayload)) registeredClaims = getRegisteredClaims(decodedJwt) yield JwtClaims.ClaimsHP(headerClaims, payloadClaims, registeredClaims) } diff --git a/oath/core/src/main/scala/io/oath/JwtVerifyError.scala b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala index 9ba7876..c435096 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifyError.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala @@ -6,23 +6,9 @@ sealed abstract class JwtVerifyError(error: String, cause: Option[Throwable] = N extends Exception(error, cause.orNull) object JwtVerifyError { - case class IllegalArgument(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) - - case class AlgorithmMismatch(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) - - case class DecodingError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) - - case class VerificationError(message: String, underlying: Option[Throwable] = None) + final case class VerificationError(message: String, underlying: Option[Throwable] = None) extends JwtVerifyError(message, underlying) - case class SignatureVerificationError(message: String, underlying: Throwable) + final case class DecodingError(message: String, underlying: Throwable) extends JwtVerifyError(message, underlying.some) - - case class DecryptionError(message: String) extends JwtVerifyError(message) - - case class TokenExpired(message: String, underlying: Option[Throwable] = None) - extends JwtVerifyError(message, underlying) - - case class UnexpectedError(message: String, underlying: Option[Throwable] = None) - extends JwtVerifyError(message, underlying) } diff --git a/oath/core/src/main/scala/io/oath/config/EncryptConfig.scala b/oath/core/src/main/scala/io/oath/config/EncryptConfig.scala deleted file mode 100644 index 4dc1f0a..0000000 --- a/oath/core/src/main/scala/io/oath/config/EncryptConfig.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.oath.config - -import com.typesafe.config.Config - -import scala.util.chaining.* - -private[oath] final case class EncryptConfig(secret: String) - -object EncryptConfig { - private def loadOrThrowEncryptConfig(encryptScoped: Config): EncryptConfig = - encryptScoped - .getMaybeNonEmptyString("secret") - .getOrElse(throw new IllegalArgumentException("Empty string for secret is not allowed for encryption!")) - .pipe(EncryptConfig.apply) - - private[config] def loadOrThrow(encryptScoped: Config): EncryptConfig = loadOrThrowEncryptConfig(encryptScoped) -} diff --git a/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala index 8546d8b..6c4b6ec 100644 --- a/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala @@ -6,12 +6,11 @@ import io.oath.config.JwtIssuerConfig.RegisteredConfig import scala.concurrent.duration.FiniteDuration -final case class JwtIssuerConfig(algorithm: Algorithm, encrypt: Option[EncryptConfig], registered: RegisteredConfig) +final case class JwtIssuerConfig(algorithm: Algorithm, registered: RegisteredConfig) object JwtIssuerConfig { inline private val IssuerConfigLocation = "issuer" inline private val AlgorithmConfigLocation = "algorithm" - inline private val EncryptConfigLocation = "encrypt" inline private val RegisteredConfigLocation = "registered" final case class RegisteredConfig( @@ -43,20 +42,18 @@ object JwtIssuerConfig { notBeforeOffset, ) - def none(): JwtIssuerConfig = JwtIssuerConfig(Algorithm.none(), None, RegisteredConfig()) + def none(): JwtIssuerConfig = JwtIssuerConfig(Algorithm.none(), RegisteredConfig()) def loadOrThrow(config: Config): JwtIssuerConfig = (for algorithmScoped <- config.getMaybeConfig(AlgorithmConfigLocation) - algorithmConfig = AlgorithmLoader.loadOrThrow(algorithmScoped, isIssuer = true) - maybeEncryptionScoped = config.getMaybeConfig(EncryptConfigLocation) - maybeEncryptConfig = maybeEncryptionScoped.map(EncryptConfig.loadOrThrow) + algorithmConfig = AlgorithmLoader.loadOrThrow(algorithmScoped, isIssuer = true) maybeRegisteredConfig = for issuerScoped <- config.getMaybeConfig(IssuerConfigLocation) registeredScoped <- issuerScoped.getMaybeConfig(RegisteredConfigLocation) yield loadOrThrowRegisteredConfig(registeredScoped) - yield JwtIssuerConfig(algorithmConfig, maybeEncryptConfig, maybeRegisteredConfig.getOrElse(RegisteredConfig()))) + yield JwtIssuerConfig(algorithmConfig, maybeRegisteredConfig.getOrElse(RegisteredConfig()))) .getOrElse(none()) def loadOrThrow(location: String): JwtIssuerConfig = diff --git a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala index 2a1058b..17eda57 100644 --- a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala @@ -2,14 +2,12 @@ package io.oath.config import com.auth0.jwt.algorithms.Algorithm import com.typesafe.config.{Config, ConfigFactory} -import io.oath.config.EncryptConfig.* import io.oath.config.JwtVerifierConfig.* import scala.concurrent.duration.FiniteDuration final case class JwtVerifierConfig( algorithm: Algorithm, - encrypt: Option[EncryptConfig], providedWith: ProvidedWithConfig, leewayWindow: LeewayWindowConfig, ) @@ -17,7 +15,6 @@ final case class JwtVerifierConfig( object JwtVerifierConfig { inline private val VerifierConfigLocation = "verifier" inline private val AlgorithmConfigLocation = "algorithm" - inline private val EncryptConfigLocation = "encrypt" inline private val ProvidedWithConfigLocation = "provided-with" inline private val LeewayWindowConfigLocation = "leeway-window" @@ -50,14 +47,12 @@ object JwtVerifierConfig { private[oath] def loadOrThrowOath(location: String): JwtVerifierConfig = JwtVerifierConfig.loadOrThrow(rootConfig.getConfig(location)) - def none(): JwtVerifierConfig = JwtVerifierConfig(Algorithm.none(), None, ProvidedWithConfig(), LeewayWindowConfig()) + def none(): JwtVerifierConfig = JwtVerifierConfig(Algorithm.none(), ProvidedWithConfig(), LeewayWindowConfig()) def loadOrThrow(config: Config): JwtVerifierConfig = (for algorithmScoped <- config.getMaybeConfig(AlgorithmConfigLocation) algorithmConfig = AlgorithmLoader.loadOrThrow(algorithmScoped, isIssuer = false) - maybeEncryptionScoped = config.getMaybeConfig(EncryptConfigLocation) - maybeEncryptConfig = maybeEncryptionScoped.map(EncryptConfig.loadOrThrow) maybeVerificationScoped = config.getMaybeConfig(VerifierConfigLocation) maybeProvidedWithConfig = for @@ -71,7 +66,6 @@ object JwtVerifierConfig { yield loadOrThrowLeewayWindowConfig(leewayWindowScoped) yield JwtVerifierConfig( algorithmConfig, - maybeEncryptConfig, maybeProvidedWithConfig.getOrElse(ProvidedWithConfig()), maybeLeewayWindowConfig.getOrElse(LeewayWindowConfig()), )).getOrElse(none()) diff --git a/oath/core/src/main/scala/io/oath/config/package.scala b/oath/core/src/main/scala/io/oath/config/package.scala index 88a1391..e380bd9 100644 --- a/oath/core/src/main/scala/io/oath/config/package.scala +++ b/oath/core/src/main/scala/io/oath/config/package.scala @@ -12,11 +12,12 @@ inline private[config] val OathLocation = "oath" private[config] val rootConfig = ConfigFactory.load().getConfig(OathLocation) extension (config: Config) { - private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { case _: ConfigException.Missing => - default + inline private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { + case _: ConfigException.Missing => + default } - private[config] def getMaybeNonEmptyString(path: String): Option[String] = + inline private[config] def getMaybeNonEmptyString(path: String): Option[String] = allCatch .withTry(Some(config.getString(path))) .recover(ifMissingDefault(Option.empty)) @@ -24,19 +25,19 @@ extension (config: Config) { .flatten .tap(value => if (value.exists(_.isEmpty)) throw new IllegalArgumentException(s"$path empty string not allowed.")) - private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = + inline private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = allCatch .withTry(Some(config.getDuration(path).toScala)) .recover(ifMissingDefault(None)) .get - private[config] def getBooleanDefaultFalse(path: String): Boolean = + inline private[config] def getBooleanDefaultFalse(path: String): Boolean = allCatch .withTry(config.getBoolean(path)) .recover(ifMissingDefault(false)) .get - private[config] def getSeqNonEmptyString(path: String): Seq[String] = + inline private[config] def getSeqNonEmptyString(path: String): Seq[String] = allCatch .withTry(config.getStringList(path).asScala.toSeq) .recover(ifMissingDefault(Seq.empty)) @@ -46,7 +47,7 @@ extension (config: Config) { throw new IllegalArgumentException(s"$path empty string in the list not allowed.") ) - private[config] def getMaybeConfig(path: String): Option[Config] = + inline private[config] def getMaybeConfig(path: String): Option[Config] = allCatch .withTry(Some(config.getConfig(path))) .recover(ifMissingDefault(Option.empty)) diff --git a/oath/core/src/main/scala/io/oath/package.scala b/oath/core/src/main/scala/io/oath/package.scala index 58a84a7..4eebe76 100644 --- a/oath/core/src/main/scala/io/oath/package.scala +++ b/oath/core/src/main/scala/io/oath/package.scala @@ -1,6 +1,16 @@ package io.oath -import io.oath.utils.* +import com.auth0.jwt.JWTCreator.Builder +import com.auth0.jwt.interfaces.DecodedJWT +import io.oath.* +import io.oath.json.ClaimsEncoder + +import java.nio.charset.StandardCharsets +import java.time.Instant +import java.util.Base64 +import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.util.chaining.scalaUtilChainingOps +import scala.util.control.Exception.allCatch // Type aliases with extra information, useful to determine the token type. type JIssuer[_] = JwtIssuer @@ -12,3 +22,51 @@ inline private def getEnumValues[A]: Set[(A, String)] = .enumValues[A] .toSet .map(value => value -> convertUpperCamelToLowerHyphen(value.toString)) + +inline private def convertUpperCamelToLowerHyphen(str: String): String = + str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim + +inline private def base64DecodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = + allCatch + .withTry(new String(Base64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) + .toEither + .left + .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) + +// TODO: report bug extension methods declared private in package oath are not visible +extension (decodedJWT: DecodedJWT) { + inline private def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) + inline private def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) + inline private def getSeqAudience: Seq[String] = + Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten + inline private def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) + inline private def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) + inline private def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) + inline private def getOptionJwtID: Option[String] = Option(decodedJWT.getId) +} + +extension (builder: Builder) { + private def safeEncode[T]( + claims: T, + toBuilder: String => Builder, + )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = + allCatch + .withTry( + claimsEncoder + .encode(claims) + .pipe(toBuilder) + ) + .toEither + .left + .map(error => JwtIssueError.EncodeError("Failed when trying to encode token", error)) + + inline private def safeEncodeHeader[H](claims: H)(using + ClaimsEncoder[H] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withHeader) + + inline private def safeEncodePayload[P](claims: P)(using + ClaimsEncoder[P] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withPayload) +} diff --git a/oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala b/oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala deleted file mode 100644 index 80911be..0000000 --- a/oath/core/src/main/scala/io/oath/utils/DecryptionUtils.scala +++ /dev/null @@ -1,32 +0,0 @@ -package io.oath.utils - -import io.oath.JwtVerifyError -import io.oath.JwtVerifyError.DecryptionError - -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec -import scala.util.control.Exception.allCatch - -private[oath] object DecryptionUtils { - - private def hexStringToByte(hexString: String): Array[Byte] = { - // https://stackoverflow.com/questions/140131/convert-a-string-representation-of-a-hex-dump-to-a-byte-array-using-java - val len = hexString.length - val data = new Array[Byte](len / 2) - var i = 0 - while i < len do - data(i / 2) = ((Character.digit(hexString.charAt(i), 16) << 4) + - Character.digit(hexString.charAt(i + 1), 16)).toByte - i += 2 - data - } - - def decryptAES(message: String, secret: String): Either[DecryptionError, String] = - allCatch.withTry { - val raw = java.util.Arrays.copyOf(secret.getBytes(UTF8), 16) - val secretKeySpec = new SecretKeySpec(raw, AES) - val cipher = Cipher.getInstance(AES) - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec) - new String(cipher.doFinal(hexStringToByte(message))) - }.toEither.left.map(e => JwtVerifyError.DecryptionError(e.getMessage)) -} diff --git a/oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala b/oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala deleted file mode 100644 index b6cfc6e..0000000 --- a/oath/core/src/main/scala/io/oath/utils/EncryptionUtils.scala +++ /dev/null @@ -1,32 +0,0 @@ -package io.oath.utils - -import io.oath.JwtIssueError - -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec -import scala.util.control.Exception.allCatch - -private[oath] object EncryptionUtils { - private lazy val HexArray = "0123456789ABCDEF".toCharArray - - private def toHexString(bytes: Array[Byte]): String = - // from https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java - val hexChars = new Array[Char](bytes.length * 2) - var j = 0 - while (j < bytes.length) { - val v = bytes(j) & 0xff - hexChars(j * 2) = HexArray(v >>> 4) - hexChars(j * 2 + 1) = HexArray(v & 0x0f) - j += 1 - } - new String(hexChars) - - def encryptAES(message: String, secret: String): Either[JwtIssueError.EncryptionError, String] = - allCatch.withTry { - val raw = java.util.Arrays.copyOf(secret.getBytes(UTF8), 16) - val secretKeySpec = new SecretKeySpec(raw, AES) - val cipher = Cipher.getInstance(AES) - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec) - toHexString(cipher.doFinal(message.getBytes(UTF8))) - }.toEither.left.map(e => JwtIssueError.EncryptionError(e.getMessage)) -} diff --git a/oath/core/src/main/scala/io/oath/utils/package.scala b/oath/core/src/main/scala/io/oath/utils/package.scala deleted file mode 100644 index dfaf0aa..0000000 --- a/oath/core/src/main/scala/io/oath/utils/package.scala +++ /dev/null @@ -1,64 +0,0 @@ -package io.oath.utils - -import com.auth0.jwt.JWTCreator.Builder -import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.* -import io.oath.json.ClaimsEncoder - -import java.nio.charset.StandardCharsets -import java.time.Instant -import java.util.Base64 -import scala.jdk.CollectionConverters.CollectionHasAsScala -import scala.util.chaining.scalaUtilChainingOps -import scala.util.control.Exception.allCatch - -inline private[utils] val AES: "AES" = "AES" -inline private[utils] val UTF8: "utf-8" = "utf-8" - -private[oath] def convertUpperCamelToLowerHyphen(str: String): String = - str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim - -private[oath] def base64DecodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = - allCatch - .withTry(new String(Base64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) - .toEither - .left - .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) - -// TODO: report bug extension methods declared private in package oath are not visible -extension (decodedJWT: DecodedJWT) { - inline def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) - inline def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) - inline def getSeqAudience: Seq[String] = - Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten - inline def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) - inline def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) - inline def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) - inline def getOptionJwtID: Option[String] = Option(decodedJWT.getId) -} - -extension (builder: Builder) { - private def safeEncode[T]( - claims: T, - toBuilder: String => Builder, - )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = - allCatch - .withTry( - claimsEncoder - .encode(claims) - .pipe(toBuilder) - ) - .toEither - .left - .map(error => JwtIssueError.EncodeError(error.getMessage)) - - inline private[oath] def safeEncodeHeader[H](claims: H)(using - ClaimsEncoder[H] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withHeader) - - inline private[oath] def safeEncodePayload[P](claims: P)(using - ClaimsEncoder[P] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withPayload) -} diff --git a/oath/core/src/test/resources/algorithm-es256.conf b/oath/core/src/test/resources/algorithm-es256.conf new file mode 100644 index 0000000..c9b900c --- /dev/null +++ b/oath/core/src/test/resources/algorithm-es256.conf @@ -0,0 +1,5 @@ +algorithm { + name = "ES256" + private-key-pem-path = "src/test/secrets/es256-private.pem" + public-key-pem-path = "src/test/secrets/es256-public.pem" +} diff --git a/oath/core/src/test/resources/algorithm-es384.conf b/oath/core/src/test/resources/algorithm-es384.conf new file mode 100644 index 0000000..a210a2f --- /dev/null +++ b/oath/core/src/test/resources/algorithm-es384.conf @@ -0,0 +1,5 @@ +algorithm { + name = "ES384" + private-key-pem-path = "src/test/secrets/es384-private.pem" + public-key-pem-path = "src/test/secrets/es384-public.pem" +} diff --git a/oath/core/src/test/resources/algorithm-es512.conf b/oath/core/src/test/resources/algorithm-es512.conf new file mode 100644 index 0000000..eed5da0 --- /dev/null +++ b/oath/core/src/test/resources/algorithm-es512.conf @@ -0,0 +1,5 @@ +algorithm { + name = "ES512" + private-key-pem-path = "src/test/secrets/es512-private.pem" + public-key-pem-path = "src/test/secrets/es512-public.pem" +} diff --git a/oath/core/src/test/resources/algorithm-hsxxx.conf b/oath/core/src/test/resources/algorithm-hsxxx.conf new file mode 100644 index 0000000..9fe86a5 --- /dev/null +++ b/oath/core/src/test/resources/algorithm-hsxxx.conf @@ -0,0 +1,4 @@ +algorithm { + name = "HS256" + secret-key = "secret" +} diff --git a/oath/core/src/test/resources/algorithm-none.conf b/oath/core/src/test/resources/algorithm-none.conf new file mode 100644 index 0000000..46711e5 --- /dev/null +++ b/oath/core/src/test/resources/algorithm-none.conf @@ -0,0 +1,3 @@ +algorithm { + name = "NONE" +} diff --git a/oath/core/src/test/resources/algorithm-rsxxx.conf b/oath/core/src/test/resources/algorithm-rsxxx.conf new file mode 100644 index 0000000..fdc48ed --- /dev/null +++ b/oath/core/src/test/resources/algorithm-rsxxx.conf @@ -0,0 +1,5 @@ +algorithm { + name = "RS256" + private-key-pem-path = "src/test/secrets/rsa-private.pem" + public-key-pem-path = "src/test/secrets/rsa-public.pem" +} diff --git a/oath/core/src/test/resources/algorithm-unsupported.conf b/oath/core/src/test/resources/algorithm-unsupported.conf new file mode 100644 index 0000000..208de17 --- /dev/null +++ b/oath/core/src/test/resources/algorithm-unsupported.conf @@ -0,0 +1,3 @@ +algorithm { + name = "Boom" +} diff --git a/oath/core/src/test/resources/issuer.conf b/oath/core/src/test/resources/issuer.conf new file mode 100644 index 0000000..87e1b16 --- /dev/null +++ b/oath/core/src/test/resources/issuer.conf @@ -0,0 +1,66 @@ +default-token { + algorithm { + name = "HS256" + secret-key = "secret" + } +} + +token { + algorithm { + name = "RS256" + private-key-pem-path = "src/test/secrets/rsa-private.pem" + } + issuer { + registered { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + include-issued-at-claim = true + include-jwt-id-claim = false + expires-at-offset = 1 day + not-before-offset = 1 minute + } + } +} + +without-private-key-token { + algorithm { + name = "RS256" + public-key-pem-path = "src/test/secrets/rsa-private.pem" + } + issuer { + registered { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + include-issued-at-claim = true + include-jwt-id-claim = false + expires-at-offset = 1 day + not-before-offset = 1 minute + } + } +} + +invalid-token-empty-string { + algorithm { + name = "HS256" + secret-key = "secret" + } + issuer { + registered { + issuer-claim = "" + } + } +} + +invalid-token-wrong-type { + algorithm { + name = "HS256" + secret-key = "secret" + } + issuer { + registered { + not-before-offset = "" + } + } +} diff --git a/oath/core/src/test/resources/manager.conf b/oath/core/src/test/resources/manager.conf new file mode 100644 index 0000000..6f25c8c --- /dev/null +++ b/oath/core/src/test/resources/manager.conf @@ -0,0 +1,31 @@ +token { + algorithm { + name = "RS256" + private-key-pem-path = "src/test/secrets/rsa-private.pem" + public-key-pem-path = "src/test/secrets/rsa-public.pem" + } + issuer { + registered { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + include-issued-at-claim = true + include-jwt-id-claim = false + expires-at-offset = 1 day + not-before-offset = 1 minute + } + } + verifier { + provided-with { + issuer-claim = ${token.issuer.registered.issuer-claim} + subject-claim = ${token.issuer.registered.subject-claim} + audience-claims = ${token.issuer.registered.audience-claims} + } + leeway-window { + leeway = 1 minute + issued-at = 4 minutes + expires-at = 3 minutes + not-before = 2 minutes + } + } +} diff --git a/oath/core/src/test/resources/reference.conf b/oath/core/src/test/resources/reference.conf new file mode 100644 index 0000000..0019270 --- /dev/null +++ b/oath/core/src/test/resources/reference.conf @@ -0,0 +1,113 @@ + +token { + algorithm { + name = "RS256" + private-key-pem-path = "src/test/secrets/rsa-private.pem" + public-key-pem-path = "src/test/secrets/rsa-public.pem" + } + issuer { + registered { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + include-issued-at-claim = true + include-jwt-id-claim = false + expires-at-offset = 1 day + not-before-offset = 1 minute + } + } + verifier { + provided-with { + issuer-claim = ${token.issuer.registered.issuer-claim} + subject-claim = ${token.issuer.registered.subject-claim} + audience-claims = ${token.issuer.registered.audience-claims} + } + leeway-window { + leeway = 1 minute + issued-at = 4 minutes + expires-at = 3 minutes + not-before = 2 minutes + } + } +} + +oath { + access-token { + algorithm { + name = "HS256" + secret-key = "secret" + } + issuer { + registered { + issuer-claim = "access-token" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + include-issued-at-claim = true + include-jwt-id-claim = true + expires-at-offset = 15 minutes + not-before-offset = 0 minute + } + } + verifier { + provided-with { + issuer-claim = ${oath.access-token.issuer.registered.issuer-claim} + subject-claim = ${oath.access-token.issuer.registered.subject-claim} + audience-claims = ${oath.access-token.issuer.registered.audience-claims} + } + leeway-window { + leeway = 1 minute + issued-at = 1 minute + expires-at = 1 minute + not-before = 1 minute + } + } + } + + refresh-token = ${oath.access-token} + refresh-token { + issuer { + registered { + issuer-claim = "refresh-token" + expires-at-offset = 6 hours + } + } + verifier { + provided-with { + issuer-claim = ${oath.refresh-token.issuer.registered.issuer-claim} + } + } + } + activation-email-token = ${oath.access-token} + activation-email-token { + issuer { + registered { + issuer-claim = "activation-email-token" + expires-at-offset = 1 day + audience-claims = [] + } + } + verifier { + provided-with { + issuer-claim = ${oath.activation-email-token.issuer.registered.issuer-claim} + audience-claims = [] + } + } + } + + forgot-password-token = ${oath.access-token} + forgot-password-token { + issuer { + registered { + issuer-claim = "forgot-password-token" + expires-at-offset = 2 hours + audience-claims = [] + } + } + verifier { + provided-with { + issuer-claim = ${oath.forgot-password-token.issuer.registered.issuer-claim} + audience-claims = [] + } + } + } +} diff --git a/oath/core/src/test/resources/verifier.conf b/oath/core/src/test/resources/verifier.conf new file mode 100644 index 0000000..b192465 --- /dev/null +++ b/oath/core/src/test/resources/verifier.conf @@ -0,0 +1,71 @@ +default-token { + algorithm { + name = "HS256" + secret-key = "src/test/secrets/rsa-public.pem" + } +} + +token { + algorithm { + name = "RS256" + public-key-pem-path = "src/test/secrets/rsa-public.pem" + } + verifier { + provided-with { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + } + leeway-window { + leeway = 1 minute + issued-at = 4 minutes + expires-at = 3 minutes + not-before = 2 minutes + } + } +} + +without-public-key-token { + algorithm { + name = "RS256" + private-key-pem-path = "src/test/secrets/rsa-public.pem" + } + + verifier { + provided-with { + issuer-claim = "issuer" + subject-claim = "subject" + audience-claims = ["aud1", "aud2"] + } + leeway-window { + leeway = 1 minute + issued-at = 4 minutes + expires-at = 3 minutes + not-before = 2 minutes + } + } +} + +invalid-token-empty-string { + algorithm { + name = "HS256" + secret-key = "secret" + } + verifier { + provided-with { + issuer-claim = "" + } + } +} + +invalid-token-wrong-type { + algorithm { + name = "HS256" + secret-key = "secret" + } + verifier { + provided-with { + audience-claims = "" + } + } +} diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala new file mode 100644 index 0000000..48e3150 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -0,0 +1,189 @@ +package io.oath + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.oath.NestedHeader.nestedHeaderDecoder +import io.oath.NestedPayload.nestedPayloadDecoder +import io.oath.config.* +import io.oath.syntax.all.* +import io.oath.testkit.* + +import scala.concurrent.duration.* +import scala.jdk.CollectionConverters.ListHasAsScala +import scala.util.Try +import scala.util.chaining.* + +class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { + + val jwtVerifier = JWT + .require(Algorithm.none()) + .acceptLeeway(5) + .build() + + "JwtIssuer" should { + "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt().value + + val decodedJWT = jwtVerifier.verify(jwtClaims.token) + + Option(decodedJWT.getIssuer) shouldBe config.registered.issuerClaim + Option(decodedJWT.getSubject) shouldBe config.registered.subjectClaim + Option(decodedJWT.getAudience) + .map(_.asScala.toSeq) + .toSeq + .flatten shouldBe config.registered.audienceClaims + + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe Option.when(config.registered.includeIssueAtClaim)(now) + + if (config.registered.includeJwtIdClaim) + Option(decodedJWT.getId) should not be empty + else + Option(decodedJWT.getId) shouldBe empty + + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe config.registered.expiresAtOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe config.registered.notBeforeOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + } + + "issue token with predefine configure claims and ad-hoc registered claims" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(registeredClaims.toClaims).value + + val expectedIssuer = registeredClaims.iss orElse config.registered.issuerClaim + val expectedSubject = registeredClaims.sub orElse config.registered.subjectClaim + val expectedAudience = + if (registeredClaims.aud.nonEmpty) registeredClaims.aud else config.registered.audienceClaims + val expectedIssuedAt = registeredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now) + val expectedExpiredAt = + registeredClaims.exp orElse config.registered.expiresAtOffset.map(offset => now.plusSeconds(offset.toSeconds)) + val expectedNotBefore = + registeredClaims.nbf orElse config.registered.notBeforeOffset.map(offset => now.plusSeconds(offset.toSeconds)) + + jwtClaims.claims.registered.iss shouldBe expectedIssuer + jwtClaims.claims.registered.sub shouldBe expectedSubject + jwtClaims.claims.registered.aud shouldBe expectedAudience + jwtClaims.claims.registered.iat shouldBe expectedIssuedAt + jwtClaims.claims.registered.exp shouldBe expectedExpiredAt + jwtClaims.claims.registered.nbf shouldBe expectedNotBefore + + if (registeredClaims.jti.nonEmpty) + jwtClaims.claims.registered.jti shouldBe registeredClaims.jti + else if (config.registered.includeJwtIdClaim) + jwtClaims.claims.registered.jti should not be empty + else jwtClaims.claims.registered.jti shouldBe empty + } + + "issue token with only registered claims empty strings" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val adHocRegisteredClaims = + registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value + + val decodedJWT = jwtVerifier.verify(jwtClaims.token) + + Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss + Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub + Option(decodedJWT.getAudience) + .map(_.asScala.toSeq) + .toSeq + .flatten shouldBe jwtClaims.claims.registered.aud + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat + Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } + + "issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val adHocRegisteredClaims = + registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value + + val decodedJWT = jwtVerifier.verify(jwtClaims.token) + + Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss + Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub + Option(decodedJWT.getAudience) + .map(_.asScala.toSeq) + .toSeq + .flatten shouldBe jwtClaims.claims.registered.aud + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat + Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } + + "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => + val jwtIssuer = new JwtIssuer(config) + val jwt = jwtIssuer.issueJwt(header.toClaimsH).value + + val result = jwtVerifier + .verify(jwt.token) + .pipe(_.getHeader) + .pipe(base64DecodeToken) + .pipe(_.value) + .pipe(nestedHeaderDecoder.decode) + .value + + result shouldBe header + } + + "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => + val jwtIssuer = new JwtIssuer(config) + val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value + + val result = jwtVerifier + .verify(jwt.token) + .pipe(_.getPayload) + .pipe(base64DecodeToken) + .pipe(_.value) + .pipe(nestedPayloadDecoder.decode) + .value + + result shouldBe payload + } + + "issue token with header & payload claims" in forAll { + (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => + val jwtIssuer = new JwtIssuer(config) + val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value + + val (headerResult, payloadResult) = jwtVerifier + .verify(jwt.token) + .pipe(decodedJwt => + base64DecodeToken(decodedJwt.getHeader).value -> base64DecodeToken(decodedJwt.getPayload).value + ) + .pipe { case (headerJson, payloadJson) => + (nestedHeaderDecoder.decode(headerJson).value, nestedPayloadDecoder.decode(payloadJson).value) + } + + headerResult shouldBe header + payloadResult shouldBe payload + } + + "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { + (config: JwtIssuerConfig) => + val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) + val jwt = jwtIssuer.issueJwt() + + val signError = jwt.left.value + .asInstanceOf[JwtIssueError.SignError] + + signError.message shouldBe "Signing token failed" + signError.underlying shouldBe a[IllegalArgumentException] + signError.underlying.getMessage shouldBe "The Algorithm cannot be null." + } + } +} diff --git a/oath/core/src/test/scala/io/oath/NestedHeader.scala b/oath/core/src/test/scala/io/oath/NestedHeader.scala new file mode 100644 index 0000000..cc5630b --- /dev/null +++ b/oath/core/src/test/scala/io/oath/NestedHeader.scala @@ -0,0 +1,37 @@ +package io.oath + +import io.circe.* +import io.circe.generic.semiauto.* +import io.circe.parser.* +import io.circe.syntax.* +import io.oath.NestedHeader.SimpleHeader +import io.oath.json.* + +final case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) + +object NestedHeader { + final case class SimpleHeader(name: String, data: List[String]) + + given simpleHeaderCirceEncoder: Encoder[SimpleHeader] = deriveEncoder[SimpleHeader] + + given simpleHeaderCirceDecoder: Decoder[SimpleHeader] = deriveDecoder[SimpleHeader] + + given nestedHeaderCirceEncoder: Encoder[NestedHeader] = deriveEncoder[NestedHeader] + + given nestedHeaderCirceDecoder: Decoder[NestedHeader] = deriveDecoder[NestedHeader] + + given simpleHeaderEncoder: ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces + + given simpleHeaderDecoder: ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") + + given nestedHeaderEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces + + given nestedHeaderDecoder: ClaimsDecoder[NestedHeader] = nestedHeaderJson => + parse(nestedHeaderJson).left + .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) + .flatMap( + _.as[NestedHeader].left.map(decodingFailure => + JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) + ) + ) +} diff --git a/oath/core/src/test/scala/io/oath/NestedPayload.scala b/oath/core/src/test/scala/io/oath/NestedPayload.scala new file mode 100644 index 0000000..fed23ca --- /dev/null +++ b/oath/core/src/test/scala/io/oath/NestedPayload.scala @@ -0,0 +1,37 @@ +package io.oath + +import io.circe.generic.semiauto.* +import io.circe.parser.* +import io.circe.syntax.* +import io.circe.{Decoder, Encoder} +import io.oath.NestedPayload.SimplePayload +import io.oath.json.* + +final case class NestedPayload(name: String, mapping: Map[String, SimplePayload]) + +object NestedPayload { + final case class SimplePayload(name: String, data: List[String]) + + given simplePayloadCirceEncoder: Encoder[SimplePayload] = deriveEncoder[SimplePayload] + + given simplePayloadCirceDecoder: Decoder[SimplePayload] = deriveDecoder[SimplePayload] + + given nestedPayloadCirceEncoder: Encoder[NestedPayload] = deriveEncoder[NestedPayload] + + given nestedPayloadCirceDecoder: Decoder[NestedPayload] = deriveDecoder[NestedPayload] + + given simplePayloadEncoder: ClaimsEncoder[SimplePayload] = simplePayload => simplePayload.asJson.noSpaces + + given simplePayloadDecoder: ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") + + given nestedPayloadEncoder: ClaimsEncoder[NestedPayload] = nestedPayload => nestedPayload.asJson.noSpaces + + given nestedPayloadDecoder: ClaimsDecoder[NestedPayload] = nestedPayloadJson => + parse(nestedPayloadJson).left + .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) + .flatMap( + _.as[NestedPayload].left.map(decodingFailure => + JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) + ) + ) +} diff --git a/oath/core/src/test/scala/io/oath/OathToken.scala b/oath/core/src/test/scala/io/oath/OathToken.scala new file mode 100644 index 0000000..d15971b --- /dev/null +++ b/oath/core/src/test/scala/io/oath/OathToken.scala @@ -0,0 +1,5 @@ +package io.oath + +enum OathToken { + case AccessToken, RefreshToken, ActivationEmailToken, ForgotPasswordToken +} diff --git a/oath/core/src/test/scala/io/oath/UtilsSpec.scala b/oath/core/src/test/scala/io/oath/UtilsSpec.scala new file mode 100644 index 0000000..42d0217 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/UtilsSpec.scala @@ -0,0 +1,29 @@ +package io.oath + +import io.oath.testkit.* + +class UtilsSpec extends AnyWordSpecBase { + + "FormatConversion" should { + "convert upper camel case to lower hyphen" in { + val res1 = convertUpperCamelToLowerHyphen("HelloWorld") + val res2 = convertUpperCamelToLowerHyphen(" Hello World ") + + val expected = "hello-world" + + res1 shouldBe expected + res2 shouldBe expected + } + + "convert scala enum string values to lower hyphen" in { + enum SomeEnum: + case firstEnum, SecondEnum, Third, ForthEnumValue + + val expected = Seq("first-enum", "second-enum", "third", "forth-enum-value") + + SomeEnum.values.toSeq + .map(_.toString) + .map(convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected + } + } +} diff --git a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala new file mode 100644 index 0000000..a06f261 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -0,0 +1,101 @@ +package io.oath.config + +import com.auth0.jwt.JWT +import com.typesafe.config.ConfigFactory +import io.oath.testkit.* + +class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { + inline val AlgorithmConfigLocation = "algorithm" + + "AlgorithmLoader" should { + "load none encryption algorithm config" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-none").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "none" + verifyingAlgorithm.getName shouldBe "none" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "load RSXXX encryption algorithm with secret key" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-rsxxx").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "RS256" + verifyingAlgorithm.getName shouldBe "RS256" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "load HSXXX encryption algorithm with secret key" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-hsxxx").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "HS256" + verifyingAlgorithm.getName shouldBe "HS256" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "load ES256 encryption algorithm with secret key" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-es256").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "ES256" + verifyingAlgorithm.getName shouldBe "ES256" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "load ES384 encryption algorithm with secret key" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-es384").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "ES384" + verifyingAlgorithm.getName shouldBe "ES384" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "load ES512 encryption algorithm with secret key" in forAll { (issuer: String) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-es512").getConfig(AlgorithmConfigLocation) + val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) + val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) + + val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) + val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer + + issuingAlgorithm.getName shouldBe "ES512" + verifyingAlgorithm.getName shouldBe "ES512" + verifiedIssuer shouldBe issuer + token should not be empty + } + + "fail to load unsupported algorithm type" in forAll { (bool: Boolean) => + val algorithmScopedConfig = ConfigFactory.load("algorithm-unsupported").getConfig(AlgorithmConfigLocation) + the[IllegalArgumentException] thrownBy AlgorithmLoader + .loadOrThrow(algorithmScopedConfig, bool) should have message "Unsupported signature algorithm: Boom" + } + } +} diff --git a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala new file mode 100644 index 0000000..bfa94d7 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -0,0 +1,77 @@ +package io.oath.config + +import com.typesafe.config.{ConfigException, ConfigFactory} +import io.oath.testkit.* + +import scala.concurrent.duration.DurationInt + +class JwtIssuerLoaderSpec extends AnyWordSpecBase { + + inline val configFile = "issuer" + inline val DefaultTokenConfigLocation = "default-token" + inline val TokenConfigLocation = "token" + inline val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" + inline val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" + inline val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + + "IssuerLoader" should { + "load default-token issuer config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) + val config = JwtIssuerConfig.loadOrThrow(configLoader) + + config.registered.issuerClaim shouldBe None + config.registered.subjectClaim shouldBe None + config.registered.audienceClaims shouldBe Seq.empty + config.registered.includeIssueAtClaim shouldBe false + config.registered.includeJwtIdClaim shouldBe false + config.registered.expiresAtOffset shouldBe None + config.registered.notBeforeOffset shouldBe None + config.algorithm.getName shouldBe "HS256" + } + + "load token issuer config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) + val config = JwtIssuerConfig.loadOrThrow(configLoader) + + config.registered.issuerClaim shouldBe Some("issuer") + config.registered.subjectClaim shouldBe Some("subject") + config.registered.audienceClaims shouldBe Seq("aud1", "aud2") + config.registered.includeIssueAtClaim shouldBe true + config.registered.includeJwtIdClaim shouldBe false + config.registered.expiresAtOffset shouldBe Some(1.day) + config.registered.notBeforeOffset shouldBe Some(1.minute) + config.algorithm.getName shouldBe "RS256" + } + + "load token issuer config values from reference.conf file using location" in { + val config = JwtIssuerConfig.loadOrThrow(TokenConfigLocation) + + config.registered.issuerClaim shouldBe Some("issuer") + config.registered.subjectClaim shouldBe Some("subject") + config.registered.audienceClaims shouldBe Seq("aud1", "aud2") + config.registered.includeIssueAtClaim shouldBe true + config.registered.includeJwtIdClaim shouldBe false + config.registered.expiresAtOffset shouldBe Some(1.day) + config.registered.notBeforeOffset shouldBe Some(1.minute) + config.algorithm.getName shouldBe "RS256" + } + + "fail to load without-private-key-token issuer config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPrivateKeyTokenConfigLocation) + + the[ConfigException.Missing] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } + + "fail to load invalid-token-empty-string issuer config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) + + the[IllegalArgumentException] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } + + "fail to load invalid-token-wrong-type issuer config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) + + the[ConfigException.BadValue] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } + } +} diff --git a/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala new file mode 100644 index 0000000..917f95e --- /dev/null +++ b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala @@ -0,0 +1,37 @@ +package io.oath.config + +import com.typesafe.config.ConfigFactory +import io.oath.testkit.* + +import scala.concurrent.duration.DurationInt + +class JwtManagerLoaderSpec extends AnyWordSpecBase { + + val configFile = "manager" + val TokenConfigLocation = "token" + + "ManagerLoader" should { + "load default-token verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) + val config = JwtManagerConfig.loadOrThrow(configLoader) + + config.issuer.registered.issuerClaim shouldBe Some("issuer") + config.issuer.registered.subjectClaim shouldBe Some("subject") + config.issuer.registered.audienceClaims shouldBe Seq("aud1", "aud2") + config.issuer.registered.includeIssueAtClaim shouldBe true + config.issuer.registered.includeJwtIdClaim shouldBe false + config.issuer.registered.expiresAtOffset shouldBe Some(1.day) + config.issuer.registered.notBeforeOffset shouldBe Some(1.minute) + config.issuer.algorithm.getName shouldBe "RS256" + + config.verifier.providedWith.issuerClaim shouldBe Some("issuer") + config.verifier.providedWith.subjectClaim shouldBe Some("subject") + config.verifier.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") + config.verifier.leewayWindow.leeway shouldBe Some(1.minute) + config.verifier.leewayWindow.issuedAt shouldBe Some(4.minutes) + config.verifier.leewayWindow.expiresAt shouldBe Some(3.minutes) + config.verifier.leewayWindow.notBefore shouldBe Some(2.minutes) + config.verifier.algorithm.getName shouldBe "RS256" + } + } +} diff --git a/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala new file mode 100644 index 0000000..0e77572 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -0,0 +1,78 @@ +package io.oath.config + +import com.typesafe.config.{ConfigException, ConfigFactory} +import io.oath.testkit.* + +import scala.concurrent.duration.DurationInt + +class JwtVerifierLoaderSpec extends AnyWordSpecBase { + + val configFile = "verifier" + val DefaultTokenConfigLocation = "default-token" + val TokenConfigLocation = "token" + val WithoutPublicKeyTokenConfigLocation = "without-public-key-token" + val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" + val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + + "VerifierLoader" should { + "load default-token verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) + val config = JwtVerifierConfig.loadOrThrow(configLoader) + + config.providedWith.issuerClaim shouldBe None + config.providedWith.subjectClaim shouldBe None + config.providedWith.audienceClaims shouldBe Seq.empty + config.leewayWindow.leeway shouldBe None + config.leewayWindow.expiresAt shouldBe None + config.leewayWindow.issuedAt shouldBe None + config.leewayWindow.expiresAt shouldBe None + config.leewayWindow.notBefore shouldBe None + config.algorithm.getName shouldBe "HS256" + } + + "load token verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) + val config = JwtVerifierConfig.loadOrThrow(configLoader) + + config.providedWith.issuerClaim shouldBe Some("issuer") + config.providedWith.subjectClaim shouldBe Some("subject") + config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") + config.leewayWindow.leeway shouldBe Some(1.minute) + config.leewayWindow.issuedAt shouldBe Some(4.minutes) + config.leewayWindow.expiresAt shouldBe Some(3.minutes) + config.leewayWindow.notBefore shouldBe Some(2.minutes) + config.algorithm.getName shouldBe "RS256" + } + + "load token verifier config values from reference.conf file using location" in { + val config = JwtVerifierConfig.loadOrThrow(TokenConfigLocation) + + config.providedWith.issuerClaim shouldBe Some("issuer") + config.providedWith.subjectClaim shouldBe Some("subject") + config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") + config.leewayWindow.leeway shouldBe Some(1.minute) + config.leewayWindow.issuedAt shouldBe Some(4.minutes) + config.leewayWindow.expiresAt shouldBe Some(3.minutes) + config.leewayWindow.notBefore shouldBe Some(2.minutes) + config.algorithm.getName shouldBe "RS256" + } + + "fail to load without-public-key-token verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPublicKeyTokenConfigLocation) + + the[ConfigException.Missing] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } + + "fail to load invalid-token-empty-string verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) + + the[IllegalArgumentException] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } + + "fail to load invalid-token-wrong-type verifier config values from configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) + + the[ConfigException.WrongType] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } + } +} diff --git a/oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala b/oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala new file mode 100644 index 0000000..0fdce24 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala @@ -0,0 +1,7 @@ +package io.oath.testkit + +import org.scalatest.matchers.should +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.{EitherValues, OptionValues} + +abstract class AnyWordSpecBase extends AnyWordSpec, should.Matchers, OptionValues, EitherValues diff --git a/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala new file mode 100644 index 0000000..f85efeb --- /dev/null +++ b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala @@ -0,0 +1,141 @@ +package io.oath.testkit + +import com.auth0.jwt.algorithms.Algorithm +import io.oath.NestedHeader.SimpleHeader +import io.oath.NestedPayload.SimplePayload +import io.oath.* +import io.oath.config.JwtIssuerConfig.RegisteredConfig +import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} +import io.oath.config.* +import org.scalacheck.* + +import java.time.Instant +import scala.concurrent.duration.{Duration, DurationInt} + +trait Arbitraries { + private lazy val genPositiveFiniteDuration = Gen.posNum[Long].map(Duration.fromNanos) + private lazy val genPositiveFiniteDurationSeconds = Gen.posNum[Int].map(x => (x + 1).seconds) + + implicit val arbNonEmptyString: Arbitrary[String] = + Arbitrary( + Gen.nonEmptyListOf[Char](Gen.alphaChar).map(_.mkString) + ) + + implicit val arbInstant: Arbitrary[Instant] = + Arbitrary( + Gen.chooseNum(Long.MinValue, Long.MaxValue).map(Instant.ofEpochMilli) + ) + + implicit val arbJwtIssuerConfig: Arbitrary[JwtIssuerConfig] = + Arbitrary { + for { + issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) + subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) + audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) + includeJwtIdClaim <- Arbitrary.arbitrary[Boolean] + includeIssueAtClaim <- Arbitrary.arbitrary[Boolean] + expiresAtOffset <- Gen.option(genPositiveFiniteDuration) + notBeforeOffset <- Gen.option(genPositiveFiniteDuration) + registered = RegisteredConfig( + issuerClaim, + subjectClaim, + audienceClaims, + includeJwtIdClaim, + includeIssueAtClaim, + expiresAtOffset, + notBeforeOffset, + ) + } yield JwtIssuerConfig(Algorithm.none(), registered) + } + + implicit val arbJwtVerifierConfig: Arbitrary[JwtVerifierConfig] = + Arbitrary { + for { + encryptKey <- Gen.option(arbNonEmptyString.arbitrary) + issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) + subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) + audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) + leeway <- Gen.option(genPositiveFiniteDurationSeconds) + issuedAt <- Gen.option(genPositiveFiniteDurationSeconds) + expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) + notBefore <- Gen.option(genPositiveFiniteDurationSeconds) + leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBefore) + providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) + } yield JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) + } + + implicit val arbJwtManagerConfig: Arbitrary[JwtManagerConfig] = + Arbitrary { + for { + encryptKey <- Gen.option(arbNonEmptyString.arbitrary) + issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) + subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) + audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) + includeJwtIdClaim <- Arbitrary.arbitrary[Boolean] + includeIssueAtClaim <- Arbitrary.arbitrary[Boolean] + expiresAtOffset <- Gen.option(genPositiveFiniteDurationSeconds) + notBeforeOffset <- Gen.option(genPositiveFiniteDurationSeconds) + leeway <- Gen.option(genPositiveFiniteDurationSeconds) + issuedAt <- Gen.option(genPositiveFiniteDurationSeconds) + expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) + leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBeforeOffset.map(_.plus(1.second))) + providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) + registered = RegisteredConfig( + issuerClaim, + subjectClaim, + audienceClaims, + includeJwtIdClaim, + includeIssueAtClaim, + expiresAtOffset, + notBeforeOffset, + ) + verifier = JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) + issuer = JwtIssuerConfig(Algorithm.none(), registered) + } yield JwtManagerConfig(issuer, verifier) + } + + implicit val arbRegisteredClaims: Arbitrary[RegisteredClaims] = + Arbitrary { + for { + iss <- Gen.option(arbNonEmptyString.arbitrary) + sub <- Gen.option(arbNonEmptyString.arbitrary) + aud <- Gen.listOf(arbNonEmptyString.arbitrary) + exp <- Gen.option(arbInstant.arbitrary) + nbf <- Gen.option(arbInstant.arbitrary) + iat <- Gen.option(arbInstant.arbitrary) + jti <- Gen.option(arbNonEmptyString.arbitrary) + } yield RegisteredClaims(iss, sub, aud, exp, nbf, iat, jti) + } + + implicit val arbSimplePayload: Arbitrary[SimplePayload] = + Arbitrary { + for { + name <- Gen.alphaStr + data <- Gen.listOf(Gen.alphaStr) + } yield SimplePayload(name, data) + } + + implicit val arbSimpleHeader: Arbitrary[SimpleHeader] = + Arbitrary { + for { + name <- Gen.alphaStr + data <- Gen.listOf(Gen.alphaStr) + } yield SimpleHeader(name, data) + } + + implicit val arbNestedPayload: Arbitrary[NestedPayload] = + Arbitrary { + for { + name <- Gen.alphaStr + mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimplePayload.arbitrary.map((str, _)))) + } yield NestedPayload(name, mapping) + } + + implicit val arbNestedHeader: Arbitrary[NestedHeader] = + Arbitrary { + for { + name <- Gen.alphaStr + mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimpleHeader.arbitrary.map((str, _)))) + } yield NestedHeader(name, mapping) + } +} diff --git a/oath/core/src/test/scala/io/oath/testkit/ClockHelper.scala b/oath/core/src/test/scala/io/oath/testkit/ClockHelper.scala new file mode 100644 index 0000000..1d322a9 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/testkit/ClockHelper.scala @@ -0,0 +1,10 @@ +package io.oath.testkit + +import java.time.temporal.ChronoUnit +import java.time.{Clock, Instant, ZoneId} + +trait ClockHelper { + def getInstantNowSeconds: Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) + + def getFixedClock(time: Instant): Clock = Clock.fixed(time, ZoneId.of("UTC")) +} diff --git a/oath/core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala b/oath/core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala new file mode 100644 index 0000000..8a2f75b --- /dev/null +++ b/oath/core/src/test/scala/io/oath/testkit/PropertyBasedTesting.scala @@ -0,0 +1,11 @@ +package io.oath.testkit + +import org.scalactic.anyvals.PosInt +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks + +trait PropertyBasedTesting extends ScalaCheckPropertyChecks, Arbitraries { + val minSuccessful = PosInt(25) + + override implicit val generatorDrivenConfig: PropertyCheckConfiguration = + PropertyCheckConfiguration(minSuccessful) +} diff --git a/oath/core/src/test/secrets/es256-private.pem b/oath/core/src/test/secrets/es256-private.pem new file mode 100644 index 0000000..756361a --- /dev/null +++ b/oath/core/src/test/secrets/es256-private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPGJGAm4X1fvBuC1z +SpO/4Izx6PXfNMaiKaS5RUkFqEGhRANCAARCBvmeksd3QGTrVs2eMrrfa7CYF+sX +sjyGg+Bo5mPKGH4Gs8M7oIvoP9pb/I85tdebtKlmiCZHAZE5w4DfJSV6 +-----END PRIVATE KEY----- diff --git a/oath/core/src/test/secrets/es256-public.pem b/oath/core/src/test/secrets/es256-public.pem new file mode 100644 index 0000000..34401f7 --- /dev/null +++ b/oath/core/src/test/secrets/es256-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQgb5npLHd0Bk61bNnjK632uwmBfr +F7I8hoPgaOZjyhh+BrPDO6CL6D/aW/yPObXXm7SpZogmRwGROcOA3yUleg== +-----END PUBLIC KEY----- diff --git a/oath/core/src/test/secrets/es384-private.pem b/oath/core/src/test/secrets/es384-private.pem new file mode 100644 index 0000000..9482bfa --- /dev/null +++ b/oath/core/src/test/secrets/es384-private.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCVWQsOJHjKD0I4cXOY +Jm4G8i5c7IMhFbxFq57OUlrTVmND43dvvNW1oQ6i6NiXEQWhZANiAASezSGlAu4w +AaJe4676mQM0F/5slI+EkdptRJdfsQP9mNxe7RdzHgcSw7j/Wxa45nlnFnFrPPL4 +viJKOBRxMB1jjVA9my9PixxJGoB22qDQwFbP8ldmEp6abwdBsXNaePM= +-----END PRIVATE KEY----- diff --git a/oath/core/src/test/secrets/es384-public.pem b/oath/core/src/test/secrets/es384-public.pem new file mode 100644 index 0000000..511596e --- /dev/null +++ b/oath/core/src/test/secrets/es384-public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEns0hpQLuMAGiXuOu+pkDNBf+bJSPhJHa +bUSXX7ED/ZjcXu0Xcx4HEsO4/1sWuOZ5ZxZxazzy+L4iSjgUcTAdY41QPZsvT4sc +SRqAdtqg0MBWz/JXZhKemm8HQbFzWnjz +-----END PUBLIC KEY----- diff --git a/oath/core/src/test/secrets/es512-private.pem b/oath/core/src/test/secrets/es512-private.pem new file mode 100644 index 0000000..bde9098 --- /dev/null +++ b/oath/core/src/test/secrets/es512-private.pem @@ -0,0 +1,7 @@ +-----BEGIN PRIVATE KEY----- +MIHtAgEAMBAGByqGSM49AgEGBSuBBAAjBIHVMIHSAgEBBEHzl1DpZSQJ8YhCbN/u +vo5SOu0BjDDX9Gub6zsBW6B2TxRzb5sBeQaWVscDUZha4Xr1HEWpVtua9+nEQU/9 +Aq9Pl6GBiQOBhgAEAJhvCa6S89ePqlLO6MRV9KQqHvdAITDAf/WRDcvCmfrrNuov ++j4gQXO12ohIukPCHM9rYms8Eqciz3gaxVTxZD4CAA8i2k9H6ew9iSh1qXa1kLxi +yzMBqmAmmg4u/SroD6OleG56SwZVbWx+KIINB6r/PQVciGX8FjwgR/mbLHotVZYD +-----END PRIVATE KEY----- diff --git a/oath/core/src/test/secrets/es512-public.pem b/oath/core/src/test/secrets/es512-public.pem new file mode 100644 index 0000000..360209a --- /dev/null +++ b/oath/core/src/test/secrets/es512-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAmG8JrpLz14+qUs7oxFX0pCoe90Ah +MMB/9ZENy8KZ+us26i/6PiBBc7XaiEi6Q8Icz2tiazwSpyLPeBrFVPFkPgIADyLa +T0fp7D2JKHWpdrWQvGLLMwGqYCaaDi79KugPo6V4bnpLBlVtbH4ogg0Hqv89BVyI +ZfwWPCBH+Zssei1VlgM= +-----END PUBLIC KEY----- diff --git a/oath/core/src/test/secrets/rsa-private.pem b/oath/core/src/test/secrets/rsa-private.pem new file mode 100644 index 0000000..1427e0d --- /dev/null +++ b/oath/core/src/test/secrets/rsa-private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4ZtdaIrd1BPIJ +tfnF0TjIK5inQAXZ3XlCrUlJdP+XHwIRxdv1FsN12XyMYO/6ymLmo9ryoQeIrsXB +XYqlET3zfAY+diwCb0HEsVvhisthwMU4gZQu6TYW2s9LnXZB5rVtcBK69hcSlA2k +ZudMZWxZcj0L7KMfO2rIvaHw/qaVOE9j0T257Z8Kp2CLF9MUgX0ObhIsdumFRLaL +DvDUmBPr2zuh/34j2XmWwn1yjN/WvGtdfhXW79Ki1S40HcWnygHgLV8sESFKUxxQ +mKvPUTwDOIwLFL5WtE8Mz7N++kgmDcmWMCHc8kcOIu73Ta/3D4imW7VbKgHZo9+K +3ESFE3RjAgMBAAECggEBAJTEIyjMqUT24G2FKiS1TiHvShBkTlQdoR5xvpZMlYbN +tVWxUmrAGqCQ/TIjYnfpnzCDMLhdwT48Ab6mQJw69MfiXwc1PvwX1e9hRscGul36 +ryGPKIVQEBsQG/zc4/L2tZe8ut+qeaK7XuYrPp8bk/X1e9qK5m7j+JpKosNSLgJj +NIbYsBkG2Mlq671irKYj2hVZeaBQmWmZxK4fw0Istz2WfN5nUKUeJhTwpR+JLUg4 +ELYYoB7EO0Cej9UBG30hbgu4RyXA+VbptJ+H042K5QJROUbtnLWuuWosZ5ATldwO +u03dIXL0SH0ao5NcWBzxU4F2sBXZRGP2x/jiSLHcqoECgYEA4qD7mXQpu1b8XO8U +6abpKloJCatSAHzjgdR2eRDRx5PMvloipfwqA77pnbjTUFajqWQgOXsDTCjcdQui +wf5XAaWu+TeAVTytLQbSiTsBhrnoqVrr3RoyDQmdnwHT8aCMouOgcC5thP9vQ8Us +rVdjvRRbnJpg3BeSNimH+u9AHgsCgYEA0EzcbOltCWPHRAY7B3Ge/AKBjBQr86Kv +TdpTlxePBDVIlH+BM6oct2gaSZZoHbqPjbq5v7yf0fKVcXE4bSVgqfDJ/sZQu9Lp +PTeV7wkk0OsAMKk7QukEpPno5q6tOTNnFecpUhVLLlqbfqkB2baYYwLJR3IRzboJ +FQbLY93E8gkCgYB+zlC5VlQbbNqcLXJoImqItgQkkuW5PCgYdwcrSov2ve5r/Acz +FNt1aRdSlx4176R3nXyibQA1Vw+ztiUFowiP9WLoM3PtPZwwe4bGHmwGNHPIfwVG +m+exf9XgKKespYbLhc45tuC08DATnXoYK7O1EnUINSFJRS8cezSI5eHcbQKBgQDC +PgqHXZ2aVftqCc1eAaxaIRQhRmY+CgUjumaczRFGwVFveP9I6Gdi+Kca3DE3F9Pq +PKgejo0SwP5vDT+rOGHN14bmGJUMsX9i4MTmZUZ5s8s3lXh3ysfT+GAhTd6nKrIE +kM3Nh6HWFhROptfc6BNusRh1kX/cspDplK5x8EpJ0QKBgQDWFg6S2je0KtbV5PYe +RultUEe2C0jYMDQx+JYxbPmtcopvZQrFEur3WKVuLy5UAy7EBvwMnZwIG7OOohJb +vkSpADK6VPn9lbqq7O8cTedEHttm6otmLt8ZyEl3hZMaL3hbuRj6ysjmoFKx6CrX +rK0/Ikt5ybqUzKCMJZg2VKGTxg== +-----END PRIVATE KEY----- diff --git a/oath/core/src/test/secrets/rsa-public.pem b/oath/core/src/test/secrets/rsa-public.pem new file mode 100644 index 0000000..e8d6288 --- /dev/null +++ b/oath/core/src/test/secrets/rsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4 +yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9 +83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs +WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT +69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8 +AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0 +YwIDAQAB +-----END PUBLIC KEY----- diff --git a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala index 8c3923d..a88d8c6 100644 --- a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala +++ b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala @@ -11,7 +11,7 @@ import io.oath.syntax.* import io.oath.testkit.AnyWordSpecBase import io.oath.utils.CodecUtils -class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils: +class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils { val verifierConfig = JwtVerifierConfig( @@ -57,3 +57,4 @@ class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils: summon[ClaimsDecoder[Bar]].decode(barJson).left.value shouldBe a[JwtVerifyError.DecodingError] } } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 72dcc01..15d966c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,31 +2,33 @@ import sbt.* object Dependencies { - val scalaTestV = "3.2.19" - val scalaTestPlusCheckV = "3.2.17.0" - val scalacheckV = "1.17.1" - val javaJWTV = "4.4.0" - val configV = "1.4.3" - val bcprovV = "1.78.1" - val circeV = "0.14.7" - val jsoniterScalaV = "2.27.3" - val catsV = "2.12.0" + private lazy val scalaTestV = "3.2.19" + private lazy val scalaTestPlusCheckV = "3.2.18.0" + private lazy val scalacheckV = "1.17.1" + private lazy val javaJWTV = "4.4.0" + private lazy val configV = "1.4.3" + private lazy val bcprovV = "1.78.1" + private lazy val circeV = "0.14.7" + private lazy val jsoniterScalaV = "2.27.3" + private lazy val catsV = "2.12.0" + private lazy val tinkV = "1.14.1" - val scalaTest = "org.scalatest" %% "scalatest" % scalaTestV - val scalaTestPlusScalaCheck = "org.scalatestplus" %% "scalacheck-1-17" % scalaTestPlusCheckV - val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheckV + lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestV + lazy val scalaTestPlusScalaCheck = "org.scalatestplus" %% "scalacheck-1-17" % scalaTestPlusCheckV + lazy val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheckV - val circeCore = "io.circe" %% "circe-core" % circeV - val circeGeneric = "io.circe" %% "circe-generic" % circeV - val circeParser = "io.circe" %% "circe-parser" % circeV + lazy val circeCore = "io.circe" %% "circe-core" % circeV + lazy val circeGeneric = "io.circe" %% "circe-generic" % circeV + lazy val circeParser = "io.circe" %% "circe-parser" % circeV - val jsoniterScalacore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterScalaV - val jsoniterScalamacros = + lazy val jsoniterScalacore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterScalaV + lazy val jsoniterScalamacros = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterScalaV % "provided" - val cats = "org.typelevel" %% "cats-core" % catsV - val typesafeConfig = "com.typesafe" % "config" % configV - val bcprov = "org.bouncycastle" % "bcprov-jdk18on" % bcprovV + lazy val cats = "org.typelevel" %% "cats-core" % catsV + lazy val typesafeConfig = "com.typesafe" % "config" % configV + lazy val bcprov = "org.bouncycastle" % "bcprov-jdk18on" % bcprovV + lazy val tink = "com.google.crypto.tink" % "tink" % tinkV - val javaJWT = "com.auth0" % "java-jwt" % javaJWTV + lazy val javaJWT = "com.auth0" % "java-jwt" % javaJWTV } diff --git a/project/Projects.scala b/project/Projects.scala deleted file mode 100644 index d6f88a4..0000000 --- a/project/Projects.scala +++ /dev/null @@ -1,6 +0,0 @@ -import org.scalafmt.sbt.ScalafmtPlugin.autoImport.scalafmtOnCompile -import sbt.* -import sbt.Keys.* -import scalafix.sbt.ScalafixPlugin.autoImport.scalafixOnCompile - -object Projects {} diff --git a/project/build.properties b/project/build.properties index 49214c4..136f452 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.9.9 +sbt.version = 1.10.1 From 913736d5a75c97fc34740ae3474f00949d7501a6 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sat, 14 Sep 2024 23:30:01 +0100 Subject: [PATCH 06/15] feat: test refac --- build.sbt | 2 +- .../io/oath/circe/CirceConversionSpec.scala | 2 +- .../main/scala/io/oath/test/Arbitraries.scala | 16 +- .../src/main/scala/io/oath/test/Main.scala | 5 +- .../test/scala/io/oath/JwtIssuerSpec.scala | 61 +--- .../test/scala/io/oath/JwtVerifierSpec.scala | 118 ++------ .../io/oath/config/JwtIssuerLoaderSpec.scala | 3 - .../oath/config/JwtVerifierLoaderSpec.scala | 3 - .../src/main/scala/io/oath/JwtIssuer.scala | 213 +++++++------ .../src/main/scala/io/oath/JwtManager.scala | 74 +++-- .../src/main/scala/io/oath/JwtVerifier.scala | 191 ++++++------ .../src/main/scala/io/oath/OathIssuer.scala | 16 +- .../src/main/scala/io/oath/OathManager.scala | 16 +- .../src/main/scala/io/oath/OathVerifier.scala | 18 +- .../core/src/main/scala/io/oath/package.scala | 44 +-- .../src/main/scala/io/oath/utils/Base64.scala | 18 ++ .../main/scala/io/oath/utils/Formatter.scala | 8 + .../test/scala/io/oath/JwtIssuerSpec.scala | 31 +- .../test/scala/io/oath/JwtVerifierSpec.scala | 281 ++++++++++++++++++ .../src/test/scala/io/oath/NestedHeader.scala | 16 +- .../test/scala/io/oath/NestedPayload.scala | 16 +- .../src/test/scala/io/oath/TestData.scala | 5 + .../scala/io/oath/testkit/Arbitraries.scala | 6 +- .../scala/io/oath/testkit/CodecHelper.scala | 10 + .../FormatterSpec.scala} | 12 +- .../JsoniterConversionSpec.scala | 2 +- .../io/oath/{ => macros}/OathEnumMacro.scala | 2 +- .../scala/io/oath/{ => macros}/OathEnum.scala | 5 +- .../oath/{ => macros}/OathEnumMacroSpec.scala | 15 +- 29 files changed, 733 insertions(+), 476 deletions(-) create mode 100644 oath/core/src/main/scala/io/oath/utils/Base64.scala create mode 100644 oath/core/src/main/scala/io/oath/utils/Formatter.scala create mode 100644 oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/TestData.scala create mode 100644 oath/core/src/test/scala/io/oath/testkit/CodecHelper.scala rename oath/core/src/test/scala/io/oath/{UtilsSpec.scala => utils/FormatterSpec.scala} (58%) rename oath/macros/src/main/scala/io/oath/{ => macros}/OathEnumMacro.scala (93%) rename oath/macros/src/test/scala/io/oath/{ => macros}/OathEnum.scala (60%) rename oath/macros/src/test/scala/io/oath/{ => macros}/OathEnumMacroSpec.scala (80%) diff --git a/build.sbt b/build.sbt index 4a28ef4..7d2b4a3 100644 --- a/build.sbt +++ b/build.sbt @@ -51,7 +51,7 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( ThisBuild / Test / fork := true ThisBuild / run / fork := true -ThisBuild / Test / parallelExecution := true +ThisBuild / Test / parallelExecution := false ThisBuild / Test / testForkedParallel := true ThisBuild / scalafmtOnCompile := sys.env.getOrElse("RUN_SCALAFMT_ON_COMPILE", "false").toBoolean ThisBuild / scalafixOnCompile := sys.env.getOrElse("RUN_SCALAFIX_ON_COMPILE", "false").toBoolean diff --git a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala index a92f222..9204bde 100644 --- a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala +++ b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala @@ -27,7 +27,7 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils { RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), ) - val jwtVerifier = new JwtVerifier(verifierConfig) + val jwtVerifier = new JwtVerifierSpec(verifierConfig) val jwtIssuer = new JwtIssuer(issuerConfig) "CirceConversion" should { diff --git a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala index ed7cc5f..80e44d1 100644 --- a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala +++ b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala @@ -26,9 +26,6 @@ trait Arbitraries { Gen.chooseNum(Long.MinValue, Long.MaxValue).map(Instant.ofEpochMilli) ) - implicit val arbEncryptConfig: Arbitrary[EncryptConfig] = - Arbitrary(arbNonEmptyString.arbitrary.map(EncryptConfig.apply)) - implicit val arbJwtIssuerConfig: Arbitrary[JwtIssuerConfig] = Arbitrary { for { @@ -48,14 +45,12 @@ trait Arbitraries { expiresAtOffset, notBeforeOffset, ) - encrypt <- Gen.option(arbEncryptConfig.arbitrary) - } yield JwtIssuerConfig(Algorithm.none(), encrypt, registered) + } yield JwtIssuerConfig(Algorithm.none(), registered) } implicit val arbJwtVerifierConfig: Arbitrary[JwtVerifierConfig] = Arbitrary { for { - encryptKey <- Gen.option(arbNonEmptyString.arbitrary) issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) @@ -63,16 +58,14 @@ trait Arbitraries { issuedAt <- Gen.option(genPositiveFiniteDurationSeconds) expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) notBefore <- Gen.option(genPositiveFiniteDurationSeconds) - encrypt = encryptKey.map(EncryptConfig.apply) leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBefore) providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) - } yield JwtVerifierConfig(Algorithm.none(), encrypt, providedWith, leewayWindow) + } yield JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) } implicit val arbJwtManagerConfig: Arbitrary[JwtManagerConfig] = Arbitrary { for { - encryptKey <- Gen.option(arbNonEmptyString.arbitrary) issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) @@ -85,7 +78,6 @@ trait Arbitraries { expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBeforeOffset.map(_.plus(1.second))) providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) - encrypt = encryptKey.map(EncryptConfig.apply) registered = RegisteredConfig( issuerClaim, subjectClaim, @@ -95,8 +87,8 @@ trait Arbitraries { expiresAtOffset, notBeforeOffset, ) - verifier = JwtVerifierConfig(Algorithm.none(), encrypt, providedWith, leewayWindow) - issuer = JwtIssuerConfig(Algorithm.none(), encrypt, registered) + verifier = JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) + issuer = JwtIssuerConfig(Algorithm.none(), registered) } yield JwtManagerConfig(issuer, verifier) } diff --git a/oath/core-test/src/main/scala/io/oath/test/Main.scala b/oath/core-test/src/main/scala/io/oath/test/Main.scala index d7ca55c..9af009b 100644 --- a/oath/core-test/src/main/scala/io/oath/test/Main.scala +++ b/oath/core-test/src/main/scala/io/oath/test/Main.scala @@ -1,8 +1,7 @@ package io.oath.test -import cats.syntax.all.* import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.config.{EncryptConfig, JwtVerifierConfig} +import io.oath.config.JwtVerifierConfig import io.oath.syntax.all.* import io.oath.{JwtVerifier, RegisteredClaims} @@ -44,7 +43,7 @@ object Main extends App, Arbitraries { val defaultConfig = arbJwtVerifierConfig.arbitrary.sample.get - val jwtVerifier = new JwtVerifier(defaultConfig.copy(encrypt = EncryptConfig("secret").some)) + val jwtVerifier = new JwtVerifier(defaultConfig) val (_, builder) = setRegisteredClaims(JWT.create(), defaultConfig) diff --git a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala index 4deea66..672f919 100644 --- a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -24,7 +24,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { "issue jwt tokens" when { "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => val now = getInstantNowSeconds - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None), getFixedClock(now)) + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt().value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -32,8 +32,8 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { Option(decodedJWT.getIssuer) shouldBe config.registered.issuerClaim Option(decodedJWT.getSubject) shouldBe config.registered.subjectClaim Option(decodedJWT.getAudience) - .map(_.asScala.toSeq) - .toSeq + .map(_.asScala.to(Seq)) + .to(Seq) .flatten shouldBe config.registered.audienceClaims Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe Option.when(config.registered.includeIssueAtClaim)(now) @@ -91,7 +91,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val now = getInstantNowSeconds val adHocRegisteredClaims = registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None), getFixedClock(now)) + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -113,7 +113,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val now = getInstantNowSeconds val adHocRegisteredClaims = registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None), getFixedClock(now)) + val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -130,25 +130,14 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf } - "issue token with only registered claims encrypted" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - whenever(config.encrypt.nonEmpty): - val clock = getFixedClock(getInstantNowSeconds) - val jwtIssuer = new JwtIssuer(config, clock) - val jwt = jwtIssuer.issueJwt(registeredClaims.toClaims).value - - jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" - jwt.token.length % 16 shouldBe 0 - } - "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) + val jwtIssuer = new JwtIssuer(config) val jwt = jwtIssuer.issueJwt(header.toClaimsH).value val result = jwtVerifier .verify(jwt.token) .pipe(_.getHeader) - .pipe(base64DecodeToken) + .pipe(Base64.decodeToken) .pipe(_.value) .pipe(nestedHeaderDecoder.decode) .value @@ -156,23 +145,14 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { result shouldBe header } - "issue token with header claims encrypted" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => - whenever(config.encrypt.nonEmpty): - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(header.toClaimsH).value - - jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" - jwt.token.length % 16 shouldBe 0 - } - "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) + val jwtIssuer = new JwtIssuer(config) val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value val result = jwtVerifier .verify(jwt.token) .pipe(_.getPayload) - .pipe(base64DecodeToken) + .pipe(Base64.decodeToken) .pipe(_.value) .pipe(nestedPayloadDecoder.decode) .value @@ -180,24 +160,15 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { result shouldBe payload } - "issue token with payload claims encrypted" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => - whenever(config.encrypt.nonEmpty): - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value - - jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" - jwt.token.length % 16 shouldBe 0 - } - "issue token with header & payload claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config.copy(encrypt = None)) + val jwtIssuer = new JwtIssuer(config) val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value val (headerResult, payloadResult) = jwtVerifier .verify(jwt.token) .pipe(decodedJwt => - base64DecodeToken(decodedJwt.getHeader).value -> base64DecodeToken(decodedJwt.getPayload).value + Base64.decodeToken(decodedJwt.getHeader).value -> Base64.decodeToken(decodedJwt.getPayload).value ) .pipe { case (headerJson, payloadJson) => (nestedHeaderDecoder.decode(headerJson).value, nestedPayloadDecoder.decode(payloadJson).value) @@ -207,16 +178,6 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { payloadResult shouldBe payload } - "issue token with header & payload claims encrypted" in forAll { - (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => - whenever(config.encrypt.nonEmpty): - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value - - jwt.token should fullyMatch regex """[0123456789ABCDEF]+""" - jwt.token.length % 16 shouldBe 0 - } - "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { (config: JwtIssuerConfig) => val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) diff --git a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala index aa927ca..5f9dea2 100644 --- a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -3,8 +3,8 @@ package io.oath import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.exceptions.* import com.auth0.jwt.{JWT, JWTCreator} +import io.oath.config.JwtVerifierConfig import io.oath.config.JwtVerifierConfig.* -import io.oath.config.{EncryptConfig, JwtVerifierConfig} import io.oath.syntax.all.* import io.oath.test.NestedHeader.{SimpleHeader, nestedHeaderEncoder} import io.oath.test.NestedPayload.{SimplePayload, nestedPayloadEncoder} @@ -17,7 +17,6 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val defaultConfig = JwtVerifierConfig( Algorithm.none(), - None, ProvidedWithConfig(None, None, Nil), LeewayWindowConfig(None, None, None, None), ) @@ -52,7 +51,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper "JwtVerifier" should { "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config) val testData = setRegisteredClaims(JWT.create(), config) @@ -64,7 +63,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config) val testData = setRegisteredClaims(JWT.create(), config) @@ -77,25 +76,8 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) } - "verify a token with header that is encrypted" in forAll { - (nestedHeader: NestedHeader, config: JwtVerifierConfig, encryptConfig: EncryptConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = Some(encryptConfig))) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withHeader(unsafeParseJsonToJavaMap(nestedHeaderEncoder.encode(nestedHeader))) - .sign(config.algorithm) - .pipe(token => EncryptionUtils.encryptAES(token, encryptConfig.secret)) - .value - - val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - - verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) - } - "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config) val testData = setRegisteredClaims(JWT.create(), config) @@ -108,26 +90,9 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) } - "verify a token with payload that is encrypted" in forAll { - (nestedPayload: NestedPayload, config: JwtVerifierConfig, encryptConfig: EncryptConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = Some(encryptConfig))) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withPayload(unsafeParseJsonToJavaMap(nestedPayloadEncoder.encode(nestedPayload))) - .sign(config.algorithm) - .pipe(token => EncryptionUtils.encryptAES(token, encryptConfig.secret)) - .value - - val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - - verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) - } - "verify a token with header & payload" in forAll { (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config) val testData = setRegisteredClaims(JWT.create(), config) @@ -142,55 +107,8 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) } - "verify a token with header & payload that is encrypted" in forAll { - ( - nestedHeader: NestedHeader, - nestedPayload: NestedPayload, - config: JwtVerifierConfig, - encryptConfig: EncryptConfig, - ) => - val jwtVerifier = new JwtVerifier(config.copy(encrypt = Some(encryptConfig))) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withPayload(unsafeParseJsonToJavaMap(nestedPayloadEncoder.encode(nestedPayload))) - .withHeader(unsafeParseJsonToJavaMap(nestedHeaderEncoder.encode(nestedHeader))) - .sign(config.algorithm) - .pipe(token => EncryptionUtils.encryptAES(token, encryptConfig.secret)) - .value - - val verified = - jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - - verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) - } - - "fail to verify a token that is encrypted" in { - val encryptConfig = defaultConfig.copy(encrypt = Some(EncryptConfig("secret"))) - val jwtVerifier = new JwtVerifier(encryptConfig) - - val token = JWT - .create() - .sign(encryptConfig.algorithm) - .pipe(token => EncryptionUtils.encryptAES(token, encryptConfig.encrypt.value.secret)) - .value - - val outOfRangeLongerToken = token + "H" - val outOfRangeShorterToken = token.take(token.length - 1) - val notValid = outOfRangeShorterToken + "." - - val failedOutOfRangeLonger = jwtVerifier.verifyJwt(outOfRangeLongerToken.toToken) - val failedOutOfRangeShorter = jwtVerifier.verifyJwt(outOfRangeShorterToken.toToken) - val failedNotValid = jwtVerifier.verifyJwt(notValid.toToken) - - failedOutOfRangeLonger.left.value shouldBe a[JwtVerifyError.DecryptionError] - failedOutOfRangeShorter.left.value shouldBe a[JwtVerifyError.DecryptionError] - failedNotValid.left.value shouldBe a[JwtVerifyError.DecryptionError] - } - "fail to decode a token with header" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val header = """{"name": "name"}""" val token = JWT @@ -204,7 +122,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to decode a token with payload" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val payload = """{"name": "name"}""" val token = JWT @@ -218,7 +136,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to decode a token with header & payload" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val header = """{"name": "name"}""" val token = JWT @@ -233,7 +151,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to decode a token with header if exception raised in decoder" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val token = JWT .create() @@ -245,7 +163,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to decode a token with payload if exception raised in decoder" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val token = JWT .create() @@ -257,7 +175,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to decode a token with header & payload if exception raised in decoder" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val token = JWT .create() @@ -271,7 +189,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper "fail to verify token with VerificationError when provided with claims are not meet criteria" in { val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) - val jwtVerifier = new JwtVerifier(config) + val jwtVerifier = new JwtVerifierSpec(config) val token = JWT .create() @@ -287,7 +205,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(algorithm = null, encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = null)) val token = JWT .create() @@ -295,7 +213,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper val verified = jwtVerifier.verifyJwt(token.toToken) - verified.left.value shouldEqual JwtVerifyError.VerificationError( + verified.left.value shouldBe JwtVerifyError.VerificationError( "JwtVerifier failed with IllegalArgumentException", Some(new IllegalArgumentException("The Algorithm cannot be null.")), ) @@ -303,7 +221,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"), encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = Algorithm.HMAC256("secret"))) val token = JWT .create() @@ -321,7 +239,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"), encrypt = None)) + val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = Algorithm.HMAC256("secret2"))) val algorithm = Algorithm.HMAC256("secret1") val token = JWT .create() @@ -338,7 +256,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to verify token with TokenExpired when JWT expires" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val expiresAt = getInstantNowSeconds.minusSeconds(1) val token = JWT @@ -354,7 +272,7 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper } "fail to verify an empty string token" in { - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = new JwtVerifierSpec(defaultConfig) val token = "" val verified = jwtVerifier.verifyJwt(token.toToken) val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index a4fed60..396a38a 100644 --- a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -19,7 +19,6 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase { val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) val config = JwtIssuerConfig.loadOrThrow(configLoader) - config.encrypt shouldBe empty config.registered.issuerClaim shouldBe None config.registered.subjectClaim shouldBe None config.registered.audienceClaims shouldBe Seq.empty @@ -34,7 +33,6 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase { val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) val config = JwtIssuerConfig.loadOrThrow(configLoader) - config.encrypt shouldBe empty config.registered.issuerClaim shouldBe Some("issuer") config.registered.subjectClaim shouldBe Some("subject") config.registered.audienceClaims shouldBe Seq("aud1", "aud2") @@ -48,7 +46,6 @@ class JwtIssuerLoaderSpec extends AnyWordSpecBase { "load token issuer config values from reference configuration file using location" in { val config = JwtIssuerConfig.loadOrThrow(TokenConfigLocation) - config.encrypt shouldBe empty config.registered.issuerClaim shouldBe Some("issuer") config.registered.subjectClaim shouldBe Some("subject") config.registered.audienceClaims shouldBe Seq("aud1", "aud2") diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala index 4f3b091..729f963 100644 --- a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -19,7 +19,6 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase { val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) val config = JwtVerifierConfig.loadOrThrow(configLoader) - config.encrypt shouldBe empty config.providedWith.issuerClaim shouldBe None config.providedWith.subjectClaim shouldBe None config.providedWith.audienceClaims shouldBe Seq.empty @@ -35,7 +34,6 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase { val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) val config = JwtVerifierConfig.loadOrThrow(configLoader) - config.encrypt shouldBe empty config.providedWith.issuerClaim shouldBe Some("issuer") config.providedWith.subjectClaim shouldBe Some("subject") config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") @@ -49,7 +47,6 @@ class JwtVerifierLoaderSpec extends AnyWordSpecBase { "load token verifier config values from reference configuration file using location" in { val config = JwtVerifierConfig.loadOrThrow(TokenConfigLocation) - config.encrypt shouldBe empty config.providedWith.issuerClaim shouldBe Some("issuer") config.providedWith.subjectClaim shouldBe Some("subject") config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index ce37d03..68b46c3 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -2,7 +2,6 @@ package io.oath import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.* import io.oath.config.* import io.oath.json.ClaimsEncoder @@ -12,111 +11,129 @@ import java.util.UUID import scala.util.chaining.* import scala.util.control.Exception.allCatch -final class JwtIssuer(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) { +trait JwtIssuer { + def issueJwt(claims: JwtClaims.Claims = JwtClaims.Claims()): Either[JwtIssueError, Jwt[JwtClaims.Claims]] + def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using + ClaimsEncoder[H] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] + def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using + ClaimsEncoder[P] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] + def issueJwt[H, P]( + claims: JwtClaims.ClaimsHP[H, P] + )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] +} - private def buildJwt(builder: JWTCreator.Builder, registeredClaims: RegisteredClaims): JWTCreator.Builder = - builder - .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) - .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) - .tap(builder => builder.withAudience(registeredClaims.aud*)) - .tap(builder => registeredClaims.jti.map(str => builder.withJWTId(str))) - .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) - .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) - .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) +object JwtIssuer { - private def setRegisteredClaims(adHocRegisteredClaims: RegisteredClaims): RegisteredClaims = { - val now = Instant.now(clock).truncatedTo(ChronoUnit.SECONDS) - RegisteredClaims( - iss = adHocRegisteredClaims.iss orElse config.registered.issuerClaim, - sub = adHocRegisteredClaims.sub orElse config.registered.subjectClaim, - aud = if (adHocRegisteredClaims.aud.isEmpty) config.registered.audienceClaims else adHocRegisteredClaims.aud, - exp = adHocRegisteredClaims.exp orElse config.registered.expiresAtOffset.map(duration => - now.plusSeconds(duration.toSeconds) - ), - nbf = adHocRegisteredClaims.nbf orElse config.registered.notBeforeOffset.map(duration => - now.plusSeconds(duration.toSeconds) - ), - iat = adHocRegisteredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now), - jti = adHocRegisteredClaims.jti orElse Option - .when(config.registered.includeJwtIdClaim)( - config.registered.issuerClaim - .map(_ + "-") - .getOrElse("") - .pipe(prefix => prefix + UUID.randomUUID().toString) + private final class JavaJwtIssuerImpl(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()) extends JwtIssuer { + + private def buildJwt(builder: JWTCreator.Builder, registeredClaims: RegisteredClaims): JWTCreator.Builder = + builder + .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) + .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) + .tap(builder => builder.withAudience(registeredClaims.aud*)) + .tap(builder => registeredClaims.jti.map(str => builder.withJWTId(str))) + .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) + .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) + .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) + + private def setRegisteredClaims(adHocRegisteredClaims: RegisteredClaims): RegisteredClaims = { + val now = Instant.now(clock).truncatedTo(ChronoUnit.SECONDS) + RegisteredClaims( + iss = adHocRegisteredClaims.iss orElse config.registered.issuerClaim, + sub = adHocRegisteredClaims.sub orElse config.registered.subjectClaim, + aud = if (adHocRegisteredClaims.aud.isEmpty) config.registered.audienceClaims else adHocRegisteredClaims.aud, + exp = adHocRegisteredClaims.exp orElse config.registered.expiresAtOffset.map(duration => + now.plusSeconds(duration.toSeconds) ), - ) - } + nbf = adHocRegisteredClaims.nbf orElse config.registered.notBeforeOffset.map(duration => + now.plusSeconds(duration.toSeconds) + ), + iat = adHocRegisteredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now), + jti = adHocRegisteredClaims.jti orElse Option + .when(config.registered.includeJwtIdClaim)( + config.registered.issuerClaim + .map(_ + "-") + .getOrElse("") + .pipe(prefix => prefix + UUID.randomUUID().toString) + ), + ) + } - private def safeSign(builder: JWTCreator.Builder, algorithm: Algorithm): Either[JwtIssueError, String] = - allCatch - .withTry(builder.sign(algorithm)) - .toEither - .left - .map(e => JwtIssueError.SignError("Signing token failed", e)) + private def safeSign(builder: JWTCreator.Builder, algorithm: Algorithm): Either[JwtIssueError, String] = + allCatch + .withTry(builder.sign(algorithm)) + .toEither + .left + .map(e => JwtIssueError.SignError("Signing token failed", e)) - def issueJwt( - claims: JwtClaims.Claims = JwtClaims.Claims() - ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = { - val jwtBuilder = JWT.create() - setRegisteredClaims(claims.registered) - .pipe(registeredClaims => buildJwt(jwtBuilder, registeredClaims) -> registeredClaims) - .pipe { case (jwtBuilder, registeredClaims) => - safeSign(jwtBuilder, config.algorithm) - .map(token => - Jwt( - JwtClaims.Claims(registeredClaims), - token, + def issueJwt( + claims: JwtClaims.Claims = JwtClaims.Claims() + ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = { + val jwtBuilder = JWT.create() + setRegisteredClaims(claims.registered) + .pipe(registeredClaims => buildJwt(jwtBuilder, registeredClaims) -> registeredClaims) + .pipe { case (jwtBuilder, registeredClaims) => + safeSign(jwtBuilder, config.algorithm) + .map(token => + Jwt( + JwtClaims.Claims(registeredClaims), + token, + ) ) - ) - } - } + } + } - def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using - ClaimsEncoder[H] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = { - val jwtBuilder = JWT.create() - for - headerBuilder <- jwtBuilder.safeEncodeHeader(claims.header) - registeredClaims = setRegisteredClaims(claims.registered) - builder = buildJwt(headerBuilder, registeredClaims) - token <- safeSign(builder, config.algorithm) - jwt = Jwt( - claims.copy(registered = registeredClaims), - token, - ) - yield jwt - } + def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using + ClaimsEncoder[H] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = { + val jwtBuilder = JWT.create() + for + headerBuilder <- jwtBuilder.safeEncodeHeader(claims.header) + registeredClaims = setRegisteredClaims(claims.registered) + builder = buildJwt(headerBuilder, registeredClaims) + token <- safeSign(builder, config.algorithm) + jwt = Jwt( + claims.copy(registered = registeredClaims), + token, + ) + yield jwt + } - def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using - ClaimsEncoder[P] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = { - val jwtBuilder = JWT.create() - for - payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) - registeredClaims = setRegisteredClaims(claims.registered) - builder = buildJwt(payloadBuilder, registeredClaims) - token <- safeSign(builder, config.algorithm) - jwt = Jwt( - claims.copy(registered = registeredClaims), - token, - ) - yield jwt - } + def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using + ClaimsEncoder[P] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = { + val jwtBuilder = JWT.create() + for + payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) + registeredClaims = setRegisteredClaims(claims.registered) + builder = buildJwt(payloadBuilder, registeredClaims) + token <- safeSign(builder, config.algorithm) + jwt = Jwt( + claims.copy(registered = registeredClaims), + token, + ) + yield jwt + } - def issueJwt[H, P]( - claims: JwtClaims.ClaimsHP[H, P] - )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = { - val jwtBuilder = JWT.create() - for - payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) - headerAndPayloadBuilder <- payloadBuilder.safeEncodeHeader(claims.header) - registeredClaims = setRegisteredClaims(claims.registered) - builder = buildJwt(headerAndPayloadBuilder, registeredClaims) - token <- safeSign(builder, config.algorithm) - jwt = Jwt( - claims.copy(registered = registeredClaims), - token, - ) - yield jwt + def issueJwt[H, P]( + claims: JwtClaims.ClaimsHP[H, P] + )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = { + val jwtBuilder = JWT.create() + for + payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) + headerAndPayloadBuilder <- payloadBuilder.safeEncodeHeader(claims.header) + registeredClaims = setRegisteredClaims(claims.registered) + builder = buildJwt(headerAndPayloadBuilder, registeredClaims) + token <- safeSign(builder, config.algorithm) + jwt = Jwt( + claims.copy(registered = registeredClaims), + token, + ) + yield jwt + } } + + def apply(config: JwtIssuerConfig, clock: Clock = Clock.systemUTC()): JwtIssuer = new JavaJwtIssuerImpl(config, clock) } diff --git a/oath/core/src/main/scala/io/oath/JwtManager.scala b/oath/core/src/main/scala/io/oath/JwtManager.scala index 76c0354..ca62989 100644 --- a/oath/core/src/main/scala/io/oath/JwtManager.scala +++ b/oath/core/src/main/scala/io/oath/JwtManager.scala @@ -3,39 +3,65 @@ package io.oath import io.oath.config.* import io.oath.json.{ClaimsDecoder, ClaimsEncoder} -final class JwtManager(config: JwtManagerConfig) { - - private val issuer: JwtIssuer = new JwtIssuer(config.issuer) - private val verifier: JwtVerifier = new JwtVerifier(config.verifier) - - def issueJwt( - claims: JwtClaims.Claims = JwtClaims.Claims() - ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = issuer.issueJwt(claims) +import java.time.Clock +trait JwtManager { + def issueJwt(claims: JwtClaims.Claims = JwtClaims.Claims()): Either[JwtIssueError, Jwt[JwtClaims.Claims]] def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using ClaimsEncoder[H] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = issuer.issueJwt(claims) - + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using ClaimsEncoder[P] - ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = - issuer.issueJwt(claims) - + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] def issueJwt[H, P]( claims: JwtClaims.ClaimsHP[H, P] - )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = - issuer.issueJwt(claims) + )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] + def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] + def verifyJwt[H](jwt: JwtToken.TokenH)(using ClaimsDecoder[H]): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] + def verifyJwt[P](jwt: JwtToken.TokenP)(using ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] + def verifyJwt[H, P]( + jwt: JwtToken.TokenHP + )(using ClaimsDecoder[H], ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] +} - def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = verifier.verifyJwt(jwt) +object JwtManager { + private final class JavaJwtManagerImpl(config: JwtManagerConfig, clock: Clock) extends JwtManager { - def verifyJwt[H](jwt: JwtToken.TokenH)(using ClaimsDecoder[H]): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] = - verifier.verifyJwt(jwt) + private val issuer = JwtIssuer(config.issuer, clock) + private val verifier = JwtVerifier(config.verifier) - def verifyJwt[P](jwt: JwtToken.TokenP)(using ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] = - verifier.verifyJwt(jwt) + def issueJwt( + claims: JwtClaims.Claims = JwtClaims.Claims() + ): Either[JwtIssueError, Jwt[JwtClaims.Claims]] = issuer.issueJwt(claims) - def verifyJwt[H, P]( - jwt: JwtToken.TokenHP - )(using ClaimsDecoder[H], ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = - verifier.verifyJwt[H, P](jwt) + def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using + ClaimsEncoder[H] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = issuer.issueJwt(claims) + + def issueJwt[P](claims: JwtClaims.ClaimsP[P])(using + ClaimsEncoder[P] + ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = + issuer.issueJwt(claims) + + def issueJwt[H, P]( + claims: JwtClaims.ClaimsHP[H, P] + )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = + issuer.issueJwt(claims) + + def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = verifier.verifyJwt(jwt) + + def verifyJwt[H](jwt: JwtToken.TokenH)(using ClaimsDecoder[H]): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] = + verifier.verifyJwt(jwt) + + def verifyJwt[P](jwt: JwtToken.TokenP)(using ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] = + verifier.verifyJwt(jwt) + + def verifyJwt[H, P]( + jwt: JwtToken.TokenHP + )(using ClaimsDecoder[H], ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = + verifier.verifyJwt[H, P](jwt) + } + + def apply(config: JwtManagerConfig, clock: Clock = Clock.systemUTC()): JwtManager = + new JavaJwtManagerImpl(config, clock) } diff --git a/oath/core/src/main/scala/io/oath/JwtVerifier.scala b/oath/core/src/main/scala/io/oath/JwtVerifier.scala index 50021bf..e48b87b 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifier.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifier.scala @@ -2,109 +2,122 @@ package io.oath import com.auth0.jwt.JWT import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.* import io.oath.config.JwtVerifierConfig import io.oath.json.* +import io.oath.utils.Base64 import scala.util.chaining.scalaUtilChainingOps import scala.util.control.Exception.allCatch -final class JwtVerifier(config: JwtVerifierConfig) { +trait JwtVerifier { + def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] + def verifyJwt[H](jwt: JwtToken.TokenH)(using ClaimsDecoder[H]): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] + def verifyJwt[P](jwt: JwtToken.TokenP)(using ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] + def verifyJwt[H, P]( + jwt: JwtToken.TokenHP + )(using ClaimsDecoder[H], ClaimsDecoder[P]): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] +} - private lazy val jwtVerifier = - JWT - .require(config.algorithm) - .tap(jwtVerification => config.providedWith.issuerClaim.map(str => jwtVerification.withIssuer(str))) - .tap(jwtVerification => config.providedWith.subjectClaim.map(str => jwtVerification.withSubject(str))) - .tap(jwtVerification => - if (config.providedWith.audienceClaims.nonEmpty) - jwtVerification.withAudience(config.providedWith.audienceClaims*) - else () - ) - .tap(jwtVerification => - config.leewayWindow.leeway.map(duration => jwtVerification.acceptLeeway(duration.toSeconds)) - ) - .tap(jwtVerification => - config.leewayWindow.issuedAt.map(duration => jwtVerification.acceptIssuedAt(duration.toSeconds)) - ) - .tap(jwtVerification => - config.leewayWindow.expiresAt.map(duration => jwtVerification.acceptExpiresAt(duration.toSeconds)) - ) - .tap(jwtVerification => - config.leewayWindow.notBefore.map(duration => jwtVerification.acceptNotBefore(duration.toSeconds)) +object JwtVerifier { + private final class JavaJwtVerifierImpl(config: JwtVerifierConfig) extends JwtVerifier { + + private lazy val jwtVerifier = + JWT + .require(config.algorithm) + .tap(jwtVerification => config.providedWith.issuerClaim.map(str => jwtVerification.withIssuer(str))) + .tap(jwtVerification => config.providedWith.subjectClaim.map(str => jwtVerification.withSubject(str))) + .tap(jwtVerification => + if (config.providedWith.audienceClaims.nonEmpty) + jwtVerification.withAudience(config.providedWith.audienceClaims*) + else () + ) + .tap(jwtVerification => + config.leewayWindow.leeway.map(duration => jwtVerification.acceptLeeway(duration.toSeconds)) + ) + .tap(jwtVerification => + config.leewayWindow.issuedAt.map(duration => jwtVerification.acceptIssuedAt(duration.toSeconds)) + ) + .tap(jwtVerification => + config.leewayWindow.expiresAt.map(duration => jwtVerification.acceptExpiresAt(duration.toSeconds)) + ) + .tap(jwtVerification => + config.leewayWindow.notBefore.map(duration => jwtVerification.acceptNotBefore(duration.toSeconds)) + ) + .build() + + inline private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = + RegisteredClaims( + iss = decodedJWT.getOptionIssuer, + sub = decodedJWT.getOptionSubject, + aud = decodedJWT.getSeqAudience, + exp = decodedJWT.getOptionExpiresAt, + nbf = decodedJWT.getOptionNotBefore, + iat = decodedJWT.getOptionIssueAt, + jti = decodedJWT.getOptionJwtID, ) - .build() - inline private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = - RegisteredClaims( - iss = decodedJWT.getOptionIssuer, - sub = decodedJWT.getOptionSubject, - aud = decodedJWT.getSeqAudience, - exp = decodedJWT.getOptionExpiresAt, - nbf = decodedJWT.getOptionNotBefore, - iat = decodedJWT.getOptionIssueAt, - jti = decodedJWT.getOptionJwtID, - ) + inline private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = + Option(token) + .filter(_.nonEmpty) + .toRight(JwtVerifyError.VerificationError("JWTVerifier failed with an empty token.")) - inline private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = - Option(token) - .filter(_.nonEmpty) - .toRight(JwtVerifyError.VerificationError("JWTVerifier failed with an empty token.")) + inline private def safeDecode[T]( + decodedObject: => Either[JwtVerifyError.DecodingError, T] + ): Either[JwtVerifyError.DecodingError, T] = + allCatch + .withTry(decodedObject) + .fold(error => Left(JwtVerifyError.DecodingError(error.getMessage, error)), identity) - inline private def safeDecode[T]( - decodedObject: => Either[JwtVerifyError.DecodingError, T] - ): Either[JwtVerifyError.DecodingError, T] = - allCatch - .withTry(decodedObject) - .fold(error => Left(JwtVerifyError.DecodingError(error.getMessage, error)), identity) + inline private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = + allCatch + .withTry(jwtVerifier.verify(token)) + .toEither + .left + .map(e => JwtVerifyError.VerificationError("JwtVerifier failed with verification error", Some(e))) - inline private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = - allCatch - .withTry(jwtVerifier.verify(token)) - .toEither - .left - .map(e => JwtVerifyError.VerificationError("JwtVerifier failed with verification error", Some(e))) + def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = + for + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + registeredClaims = getRegisteredClaims(decodedJwt) + yield JwtClaims.Claims(registeredClaims) - def verifyJwt(jwt: JwtToken.Token): Either[JwtVerifyError, JwtClaims.Claims] = - for - token <- validateToken(jwt.token) - decodedJwt <- verify(token) - registeredClaims = getRegisteredClaims(decodedJwt) - yield JwtClaims.Claims(registeredClaims) + def verifyJwt[H](jwt: JwtToken.TokenH)(using + claimsDecoder: ClaimsDecoder[H] + ): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] = + for + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + json <- Base64.decodeToken(decodedJwt.getHeader) + payload <- safeDecode(claimsDecoder.decode(json)) + registeredClaims = getRegisteredClaims(decodedJwt) + yield JwtClaims.ClaimsH(payload, registeredClaims) - def verifyJwt[H](jwt: JwtToken.TokenH)(using - claimsDecoder: ClaimsDecoder[H] - ): Either[JwtVerifyError, JwtClaims.ClaimsH[H]] = - for - token <- validateToken(jwt.token) - decodedJwt <- verify(token) - json <- base64DecodeToken(decodedJwt.getHeader) - payload <- safeDecode(claimsDecoder.decode(json)) - registeredClaims = getRegisteredClaims(decodedJwt) - yield JwtClaims.ClaimsH(payload, registeredClaims) + def verifyJwt[P](jwt: JwtToken.TokenP)(using + claimsDecoder: ClaimsDecoder[P] + ): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] = + for + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + json <- Base64.decodeToken(decodedJwt.getPayload) + payload <- safeDecode(claimsDecoder.decode(json)) + registeredClaims = getRegisteredClaims(decodedJwt) + yield JwtClaims.ClaimsP(payload, registeredClaims) - def verifyJwt[P](jwt: JwtToken.TokenP)(using - claimsDecoder: ClaimsDecoder[P] - ): Either[JwtVerifyError, JwtClaims.ClaimsP[P]] = - for - token <- validateToken(jwt.token) - decodedJwt <- verify(token) - json <- base64DecodeToken(decodedJwt.getPayload) - payload <- safeDecode(claimsDecoder.decode(json)) - registeredClaims = getRegisteredClaims(decodedJwt) - yield JwtClaims.ClaimsP(payload, registeredClaims) + def verifyJwt[H, P](jwt: JwtToken.TokenHP)(using + headerDecoder: ClaimsDecoder[H], + payloadDecoder: ClaimsDecoder[P], + ): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = + for + token <- validateToken(jwt.token) + decodedJwt <- verify(token) + jsonHeader <- Base64.decodeToken(decodedJwt.getHeader) + jsonPayload <- Base64.decodeToken(decodedJwt.getPayload) + headerClaims <- safeDecode(headerDecoder.decode(jsonHeader)) + payloadClaims <- safeDecode(payloadDecoder.decode(jsonPayload)) + registeredClaims = getRegisteredClaims(decodedJwt) + yield JwtClaims.ClaimsHP(headerClaims, payloadClaims, registeredClaims) + } - def verifyJwt[H, P](jwt: JwtToken.TokenHP)(using - headerDecoder: ClaimsDecoder[H], - payloadDecoder: ClaimsDecoder[P], - ): Either[JwtVerifyError, JwtClaims.ClaimsHP[H, P]] = - for - token <- validateToken(jwt.token) - decodedJwt <- verify(token) - jsonHeader <- base64DecodeToken(decodedJwt.getHeader) - jsonPayload <- base64DecodeToken(decodedJwt.getPayload) - headerClaims <- safeDecode(headerDecoder.decode(jsonHeader)) - payloadClaims <- safeDecode(payloadDecoder.decode(jsonPayload)) - registeredClaims = getRegisteredClaims(decodedJwt) - yield JwtClaims.ClaimsHP(headerClaims, payloadClaims, registeredClaims) + def apply(config: JwtVerifierConfig): JwtVerifier = new JavaJwtVerifierImpl(config) } diff --git a/oath/core/src/main/scala/io/oath/OathIssuer.scala b/oath/core/src/main/scala/io/oath/OathIssuer.scala index cc70ded..4d6011b 100644 --- a/oath/core/src/main/scala/io/oath/OathIssuer.scala +++ b/oath/core/src/main/scala/io/oath/OathIssuer.scala @@ -1,23 +1,31 @@ package io.oath +import io.oath.OathIssuer.JIssuer import io.oath.config.JwtIssuerConfig import scala.util.chaining.scalaUtilChainingOps -final class OathIssuer[A](mapping: Map[A, JwtIssuer]) { - def as[S <: A](tokenType: S): JIssuer[S] = mapping(tokenType) +trait OathIssuer[A] { + def as[S <: A](tokenType: S): JIssuer[S] } object OathIssuer { + // Type aliases with extra information, useful to determine the token type. + type JIssuer[_] = JwtIssuer + + private final class JavaJwtOathIssuer[A](mapping: Map[A, JwtIssuer]) extends OathIssuer[A] { + def as[S <: A](tokenType: S): JIssuer[S] = mapping(tokenType) + } + inline def none[A]: OathIssuer[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtIssuer(JwtIssuerConfig.none()) }.toMap - .pipe(mapping => OathIssuer(mapping)) + .pipe(mapping => new JavaJwtOathIssuer(mapping)) inline def createOrFail[A]: OathIssuer[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtIssuerConfig.loadOrThrowOath(tokenConfig).pipe(JwtIssuer(_)) }.toMap - .pipe(mapping => OathIssuer(mapping)) + .pipe(mapping => new JavaJwtOathIssuer(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/OathManager.scala b/oath/core/src/main/scala/io/oath/OathManager.scala index 25aca0e..69aa360 100644 --- a/oath/core/src/main/scala/io/oath/OathManager.scala +++ b/oath/core/src/main/scala/io/oath/OathManager.scala @@ -1,23 +1,31 @@ package io.oath +import io.oath.OathManager.JManager import io.oath.config.* import scala.util.chaining.scalaUtilChainingOps -final class OathManager[A](mapping: Map[A, JwtManager]) { - def as[S <: A](tokenType: S): JManager[S] = mapping(tokenType) +trait OathManager[A] { + def as[S <: A](tokenType: S): JManager[S] } object OathManager { + // Type aliases with extra information, useful to determine the token type. + type JManager[_] = JwtManager + + private final class JavaJwtOathManager[A](mapping: Map[A, JwtManager]) extends OathManager[A] { + def as[S <: A](tokenType: S): JManager[S] = mapping(tokenType) + } + inline def none[A]: OathManager[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtManager(JwtManagerConfig.none()) }.toMap - .pipe(mapping => OathManager(mapping)) + .pipe(mapping => new JavaJwtOathManager(mapping)) inline def createOrFail[A]: OathManager[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtManagerConfig.loadOrThrowOath(tokenConfig).pipe(JwtManager(_)) }.toMap - .pipe(mapping => OathManager(mapping)) + .pipe(mapping => new JavaJwtOathManager(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/OathVerifier.scala b/oath/core/src/main/scala/io/oath/OathVerifier.scala index 7698056..85436d6 100644 --- a/oath/core/src/main/scala/io/oath/OathVerifier.scala +++ b/oath/core/src/main/scala/io/oath/OathVerifier.scala @@ -1,22 +1,32 @@ package io.oath +import io.oath.OathVerifier.JVerifier import io.oath.config.* import scala.util.chaining.scalaUtilChainingOps -final class OathVerifier[A](mapping: Map[A, JwtVerifier]): - def as[S <: A](tokenType: S): JVerifier[S] = mapping(tokenType) +trait OathVerifier[A] { + def as[S <: A](tokenType: S): JVerifier[S] + +} object OathVerifier { + // Type aliases with extra information, useful to determine the token type. + type JVerifier[_] = JwtVerifier + + private final class JavaJwtOathVerifier[A](mapping: Map[A, JwtVerifier]) extends OathVerifier[A] { + def as[S <: A](tokenType: S): JVerifier[S] = mapping(tokenType) + } + inline def none[A]: OathVerifier[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtVerifier(JwtVerifierConfig.none()) }.toMap - .pipe(mapping => OathVerifier(mapping)) + .pipe(mapping => new JavaJwtOathVerifier(mapping)) inline def createOrFail[A]: OathVerifier[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtVerifierConfig.loadOrThrowOath(tokenConfig).pipe(JwtVerifier(_)) }.toMap - .pipe(mapping => OathVerifier(mapping)) + .pipe(mapping => new JavaJwtOathVerifier(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/package.scala b/oath/core/src/main/scala/io/oath/package.scala index 4eebe76..0c3900b 100644 --- a/oath/core/src/main/scala/io/oath/package.scala +++ b/oath/core/src/main/scala/io/oath/package.scala @@ -2,49 +2,35 @@ package io.oath import com.auth0.jwt.JWTCreator.Builder import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.* import io.oath.json.ClaimsEncoder +import io.oath.macros.OathEnumMacro +import io.oath.utils.Formatter -import java.nio.charset.StandardCharsets import java.time.Instant -import java.util.Base64 import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.util.chaining.scalaUtilChainingOps import scala.util.control.Exception.allCatch -// Type aliases with extra information, useful to determine the token type. -type JIssuer[_] = JwtIssuer -type JManager[_] = JwtManager -type JVerifier[_] = JwtVerifier - +// TODO: Move to a file and test it properly inline private def getEnumValues[A]: Set[(A, String)] = OathEnumMacro .enumValues[A] .toSet - .map(value => value -> convertUpperCamelToLowerHyphen(value.toString)) - -inline private def convertUpperCamelToLowerHyphen(str: String): String = - str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim - -inline private def base64DecodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = - allCatch - .withTry(new String(Base64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) - .toEither - .left - .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) + .map(value => value -> Formatter.convertUpperCamelToLowerHyphen(value.toString)) -// TODO: report bug extension methods declared private in package oath are not visible +// TODO: Move to file extension (decodedJWT: DecodedJWT) { - inline private def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) - inline private def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) - inline private def getSeqAudience: Seq[String] = + inline def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) + inline def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) + inline def getSeqAudience: Seq[String] = Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten - inline private def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) - inline private def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) - inline private def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) - inline private def getOptionJwtID: Option[String] = Option(decodedJWT.getId) + inline def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) + inline def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) + inline def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) + inline def getOptionJwtID: Option[String] = Option(decodedJWT.getId) } +// TODO: Move to file extension (builder: Builder) { private def safeEncode[T]( claims: T, @@ -60,12 +46,12 @@ extension (builder: Builder) { .left .map(error => JwtIssueError.EncodeError("Failed when trying to encode token", error)) - inline private def safeEncodeHeader[H](claims: H)(using + inline def safeEncodeHeader[H](claims: H)(using ClaimsEncoder[H] ): Either[JwtIssueError.EncodeError, Builder] = safeEncode(claims, builder.withHeader) - inline private def safeEncodePayload[P](claims: P)(using + inline def safeEncodePayload[P](claims: P)(using ClaimsEncoder[P] ): Either[JwtIssueError.EncodeError, Builder] = safeEncode(claims, builder.withPayload) diff --git a/oath/core/src/main/scala/io/oath/utils/Base64.scala b/oath/core/src/main/scala/io/oath/utils/Base64.scala new file mode 100644 index 0000000..859dd82 --- /dev/null +++ b/oath/core/src/main/scala/io/oath/utils/Base64.scala @@ -0,0 +1,18 @@ +package io.oath.utils + +import io.oath.JwtVerifyError + +import java.nio.charset.StandardCharsets +import java.util.Base64 as JBase64 +import scala.util.control.Exception.allCatch + +private[oath] object Base64 { + + // TODO: not tested + inline def decodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = + allCatch + .withTry(new String(JBase64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) + .toEither + .left + .map(JwtVerifyError.DecodingError("Base64 decode failure.", _)) +} diff --git a/oath/core/src/main/scala/io/oath/utils/Formatter.scala b/oath/core/src/main/scala/io/oath/utils/Formatter.scala new file mode 100644 index 0000000..6b4c2cd --- /dev/null +++ b/oath/core/src/main/scala/io/oath/utils/Formatter.scala @@ -0,0 +1,8 @@ +package io.oath.utils + +private[oath] object Formatter { + + // TODO: not tested properly + inline def convertUpperCamelToLowerHyphen(str: String): String = + str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim +} diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala index 48e3150..7b699f3 100644 --- a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -2,11 +2,10 @@ package io.oath import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.oath.NestedHeader.nestedHeaderDecoder -import io.oath.NestedPayload.nestedPayloadDecoder import io.oath.config.* import io.oath.syntax.all.* import io.oath.testkit.* +import io.oath.utils.* import scala.concurrent.duration.* import scala.jdk.CollectionConverters.ListHasAsScala @@ -23,7 +22,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { "JwtIssuer" should { "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => val now = getInstantNowSeconds - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt().value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -54,7 +53,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { "issue token with predefine configure claims and ad-hoc registered claims" in forAll { (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => val now = getInstantNowSeconds - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt(registeredClaims.toClaims).value val expectedIssuer = registeredClaims.iss orElse config.registered.issuerClaim @@ -86,7 +85,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val now = getInstantNowSeconds val adHocRegisteredClaims = registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -108,7 +107,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { val now = getInstantNowSeconds val adHocRegisteredClaims = registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value val decodedJWT = jwtVerifier.verify(jwtClaims.token) @@ -126,30 +125,30 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { } "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => - val jwtIssuer = new JwtIssuer(config) + val jwtIssuer = JwtIssuer(config) val jwt = jwtIssuer.issueJwt(header.toClaimsH).value val result = jwtVerifier .verify(jwt.token) .pipe(_.getHeader) - .pipe(base64DecodeToken) + .pipe(Base64.decodeToken) .pipe(_.value) - .pipe(nestedHeaderDecoder.decode) + .pipe(NestedHeader.claimsDecoder.decode) .value result shouldBe header } "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config) + val jwtIssuer = JwtIssuer(config) val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value val result = jwtVerifier .verify(jwt.token) .pipe(_.getPayload) - .pipe(base64DecodeToken) + .pipe(Base64.decodeToken) .pipe(_.value) - .pipe(nestedPayloadDecoder.decode) + .pipe(NestedPayload.claimsDecoder.decode) .value result shouldBe payload @@ -157,16 +156,16 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { "issue token with header & payload claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config) + val jwtIssuer = JwtIssuer(config) val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value val (headerResult, payloadResult) = jwtVerifier .verify(jwt.token) .pipe(decodedJwt => - base64DecodeToken(decodedJwt.getHeader).value -> base64DecodeToken(decodedJwt.getPayload).value + Base64.decodeToken(decodedJwt.getHeader).value -> Base64.decodeToken(decodedJwt.getPayload).value ) .pipe { case (headerJson, payloadJson) => - (nestedHeaderDecoder.decode(headerJson).value, nestedPayloadDecoder.decode(payloadJson).value) + (NestedHeader.claimsDecoder.decode(headerJson).value, NestedPayload.claimsDecoder.decode(payloadJson).value) } headerResult shouldBe header @@ -175,7 +174,7 @@ class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { (config: JwtIssuerConfig) => - val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) + val jwtIssuer = JwtIssuer(config.copy(algorithm = null)) val jwt = jwtIssuer.issueJwt() val signError = jwt.left.value diff --git a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala new file mode 100644 index 0000000..d9d7b42 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -0,0 +1,281 @@ +package io.oath + +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.* +import com.auth0.jwt.{JWT, JWTCreator} +import io.oath.NestedHeader.SimpleHeader +import io.oath.NestedPayload.SimplePayload +import io.oath.config.JwtVerifierConfig +import io.oath.config.JwtVerifierConfig.* +import io.oath.syntax.all.* +import io.oath.testkit.* + +import scala.util.chaining.scalaUtilChainingOps + +class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { + + val defaultConfig = + JwtVerifierConfig( + Algorithm.none(), + ProvidedWithConfig(None, None, Nil), + LeewayWindowConfig(None, None, None, None), + ) + + def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig): TestData = { + val now = getInstantNowSeconds + val leeway = config.leewayWindow.leeway.map(leeway => now.plusSeconds(leeway.toSeconds - 1)) + val expiresAt = config.leewayWindow.expiresAt.map(expiresAt => now.plusSeconds(expiresAt.toSeconds - 1)) + val notBefore = config.leewayWindow.notBefore.map(notBefore => now.plusSeconds(notBefore.toSeconds - 1)) + val issueAt = config.leewayWindow.issuedAt.map(issueAt => now.plusSeconds(issueAt.toSeconds - 1)) + + val registeredClaims = RegisteredClaims( + config.providedWith.issuerClaim, + config.providedWith.subjectClaim, + config.providedWith.audienceClaims, + expiresAt orElse leeway, + notBefore orElse leeway, + issueAt orElse leeway, + None, + ) + + val builderWithRegistered = builder + .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) + .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) + .tap(builder => builder.withAudience(registeredClaims.aud*)) + .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) + .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) + .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) + + TestData(registeredClaims, builderWithRegistered) + } + + "JwtVerifier" should { + "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder.sign(config.algorithm) + + val verified = jwtVerifier.verifyJwt(token.toToken).value + + verified.registered shouldBe testData.registeredClaims + } + + "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) + .sign(config.algorithm) + + val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + + verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) + } + + "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) + .sign(config.algorithm) + + val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) + + verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) + } + + "verify a token with header & payload" in forAll { + (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) + .sign(config.algorithm) + + val verified = + jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + + verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) + } + + "fail to decode a token with header" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val header = """{"name": "name"}""" + val token = JWT + .create() + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) + .sign(defaultConfig.algorithm) + + val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + + verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } + + "fail to decode a token with payload" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val payload = """{"name": "name"}""" + val token = JWT + .create() + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(payload)) + .sign(defaultConfig.algorithm) + + val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) + + verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } + + "fail to decode a token with header & payload" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val header = """{"name": "name"}""" + val token = JWT + .create() + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) + .sign(defaultConfig.algorithm) + + val verified = + jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + + verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + } + +// "fail to decode a token with header if exception raised in decoder" in { +// val jwtVerifier = JwtVerifier(defaultConfig) +// val token = JWT +// .create() +// .sign(defaultConfig.algorithm) +// +// val verified = jwtVerifier.verifyJwt[SimpleHeader](token.toTokenH) +// +// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) +// } +// +// "fail to decode a token with payload if exception raised in decoder" in { +// val jwtVerifier = JwtVerifier(defaultConfig) +// val token = JWT +// .create() +// .sign(defaultConfig.algorithm) +// +// val verified = jwtVerifier.verifyJwt[SimplePayload](token.toTokenP) +// +// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) +// } +// +// "fail to decode a token with header & payload if exception raised in decoder" in { +// val jwtVerifier = JwtVerifier(defaultConfig) +// val token = JWT +// .create() +// .sign(defaultConfig.algorithm) +// +// val verified = +// jwtVerifier.verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) +// +// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) +// } +// +// "fail to verify token with VerificationError when provided with claims are not meet criteria" in { +// val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) +// val jwtVerifier = JwtVerifier(config) +// val token = JWT +// .create() +// .sign(config.algorithm) +// +// val verified = jwtVerifier.verifyJwt(token.toToken) +// +// verified.left.value shouldEqual JwtVerifyError.VerificationError( +// "JwtVerifier failed with JWTVerificationException", +// Some(new JWTVerificationException("The Claim 'iss' is not present in the JWT.")), +// ) +// } +// +// "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { +// (config: JwtVerifierConfig) => +// val jwtVerifier = JwtVerifier(config.copy(algorithm = null)) +// val token = JWT +// .create() +// .sign(config.algorithm) +// +// val verified = jwtVerifier.verifyJwt(token.toToken) +// +// verified.left.value shouldBe JwtVerifyError.VerificationError( +// "JwtVerifier failed with IllegalArgumentException", +// Some(new IllegalArgumentException("The Algorithm cannot be null.")), +// ) +// } +// +// "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { +// (config: JwtVerifierConfig) => +// val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"))) +// val token = JWT +// .create() +// .sign(config.algorithm) +// +// val verified = jwtVerifier.verifyJwt(token.toToken) +// +// verified.left.value shouldEqual +// JwtVerifyError +// .VerificationError( +// "JwtVerifier failed with verification error", +// Some(new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected.")), +// ) +// } +// +// "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { +// (config: JwtVerifierConfig) => +// val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"))) +// val algorithm = Algorithm.HMAC256("secret1") +// val token = JWT +// .create() +// .sign(algorithm) +// +// val verified = jwtVerifier.verifyJwt(token.toToken) +// +// verified.left.value shouldEqual +// JwtVerifyError +// .VerificationError( +// "JwtVerifier failed with SignatureVerificationException", +// null, +// ) +// } +// +// "fail to verify token with TokenExpired when JWT expires" in { +// val jwtVerifier = JwtVerifier(defaultConfig) +// val expiresAt = getInstantNowSeconds.minusSeconds(1) +// val token = JWT +// .create() +// .withExpiresAt(expiresAt) +// .sign(defaultConfig.algorithm) +// +// val verified = jwtVerifier.verifyJwt(token.toToken) +// +// verified.left.value shouldEqual +// JwtVerifyError +// .VerificationError(s"The Token has expired on $expiresAt.", null) +// } +// +// "fail to verify an empty string token" in { +// val jwtVerifier = JwtVerifier(defaultConfig) +// val token = "" +// val verified = jwtVerifier.verifyJwt(token.toToken) +// val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) +// val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) +// val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) +// +// verified.left.value shouldBe +// JwtVerifyError +// .VerificationError("JWT Token is empty.") +// +// verifiedH.left.value shouldBe +// JwtVerifyError +// .VerificationError("JWT Token is empty.") +// +// verifiedP.left.value shouldBe +// JwtVerifyError +// .VerificationError("JWT Token is empty.") +// +// verifiedHP.left.value shouldBe +// JwtVerifyError +// .VerificationError("JWT Token is empty.") +// } + } +} diff --git a/oath/core/src/test/scala/io/oath/NestedHeader.scala b/oath/core/src/test/scala/io/oath/NestedHeader.scala index cc5630b..1771650 100644 --- a/oath/core/src/test/scala/io/oath/NestedHeader.scala +++ b/oath/core/src/test/scala/io/oath/NestedHeader.scala @@ -12,21 +12,21 @@ final case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) object NestedHeader { final case class SimpleHeader(name: String, data: List[String]) - given simpleHeaderCirceEncoder: Encoder[SimpleHeader] = deriveEncoder[SimpleHeader] + given Encoder[SimpleHeader] = deriveEncoder[SimpleHeader] - given simpleHeaderCirceDecoder: Decoder[SimpleHeader] = deriveDecoder[SimpleHeader] + given Decoder[SimpleHeader] = deriveDecoder[SimpleHeader] - given nestedHeaderCirceEncoder: Encoder[NestedHeader] = deriveEncoder[NestedHeader] + given circeEncoder: Encoder[NestedHeader] = deriveEncoder[NestedHeader] - given nestedHeaderCirceDecoder: Decoder[NestedHeader] = deriveDecoder[NestedHeader] + given circeDecoder: Decoder[NestedHeader] = deriveDecoder[NestedHeader] - given simpleHeaderEncoder: ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces + given ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces - given simpleHeaderDecoder: ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") + given ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") - given nestedHeaderEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces + given claimsEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces - given nestedHeaderDecoder: ClaimsDecoder[NestedHeader] = nestedHeaderJson => + given claimsDecoder: ClaimsDecoder[NestedHeader] = nestedHeaderJson => parse(nestedHeaderJson).left .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) .flatMap( diff --git a/oath/core/src/test/scala/io/oath/NestedPayload.scala b/oath/core/src/test/scala/io/oath/NestedPayload.scala index fed23ca..3783fb9 100644 --- a/oath/core/src/test/scala/io/oath/NestedPayload.scala +++ b/oath/core/src/test/scala/io/oath/NestedPayload.scala @@ -12,21 +12,21 @@ final case class NestedPayload(name: String, mapping: Map[String, SimplePayload] object NestedPayload { final case class SimplePayload(name: String, data: List[String]) - given simplePayloadCirceEncoder: Encoder[SimplePayload] = deriveEncoder[SimplePayload] + given Encoder[SimplePayload] = deriveEncoder[SimplePayload] - given simplePayloadCirceDecoder: Decoder[SimplePayload] = deriveDecoder[SimplePayload] + given Decoder[SimplePayload] = deriveDecoder[SimplePayload] - given nestedPayloadCirceEncoder: Encoder[NestedPayload] = deriveEncoder[NestedPayload] + given circeEncoder: Encoder[NestedPayload] = deriveEncoder[NestedPayload] - given nestedPayloadCirceDecoder: Decoder[NestedPayload] = deriveDecoder[NestedPayload] + given circeDecoder: Decoder[NestedPayload] = deriveDecoder[NestedPayload] - given simplePayloadEncoder: ClaimsEncoder[SimplePayload] = simplePayload => simplePayload.asJson.noSpaces + given ClaimsEncoder[SimplePayload] = simplePayload => simplePayload.asJson.noSpaces - given simplePayloadDecoder: ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") + given ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") - given nestedPayloadEncoder: ClaimsEncoder[NestedPayload] = nestedPayload => nestedPayload.asJson.noSpaces + given claimsEncoder: ClaimsEncoder[NestedPayload] = nestedPayload => nestedPayload.asJson.noSpaces - given nestedPayloadDecoder: ClaimsDecoder[NestedPayload] = nestedPayloadJson => + given claimsDecoder: ClaimsDecoder[NestedPayload] = nestedPayloadJson => parse(nestedPayloadJson).left .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) .flatMap( diff --git a/oath/core/src/test/scala/io/oath/TestData.scala b/oath/core/src/test/scala/io/oath/TestData.scala new file mode 100644 index 0000000..53f79da --- /dev/null +++ b/oath/core/src/test/scala/io/oath/TestData.scala @@ -0,0 +1,5 @@ +package io.oath + +import com.auth0.jwt.JWTCreator + +final case class TestData(registeredClaims: RegisteredClaims, builder: JWTCreator.Builder) diff --git a/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala index f85efeb..8c22a2e 100644 --- a/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala +++ b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala @@ -1,11 +1,11 @@ package io.oath.testkit import com.auth0.jwt.algorithms.Algorithm -import io.oath.NestedHeader.SimpleHeader -import io.oath.NestedPayload.SimplePayload +import io.oath.NestedHeader.* +import io.oath.NestedPayload.* import io.oath.* import io.oath.config.JwtIssuerConfig.RegisteredConfig -import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} +import io.oath.config.JwtVerifierConfig.* import io.oath.config.* import org.scalacheck.* diff --git a/oath/core/src/test/scala/io/oath/testkit/CodecHelper.scala b/oath/core/src/test/scala/io/oath/testkit/CodecHelper.scala new file mode 100644 index 0000000..7b849f5 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/testkit/CodecHelper.scala @@ -0,0 +1,10 @@ +package io.oath.testkit + +import com.fasterxml.jackson.databind.ObjectMapper + +object CodecHelper { + private val mapper = new ObjectMapper() + + def unsafeParseJsonToJavaMap(json: String): java.util.Map[String, Object] = + mapper.readValue(json, classOf[java.util.HashMap[String, Object]]) +} diff --git a/oath/core/src/test/scala/io/oath/UtilsSpec.scala b/oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala similarity index 58% rename from oath/core/src/test/scala/io/oath/UtilsSpec.scala rename to oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala index 42d0217..2f53832 100644 --- a/oath/core/src/test/scala/io/oath/UtilsSpec.scala +++ b/oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala @@ -1,13 +1,13 @@ -package io.oath +package io.oath.utils import io.oath.testkit.* -class UtilsSpec extends AnyWordSpecBase { +class FormatterSpec extends AnyWordSpecBase { - "FormatConversion" should { + "Formatter" should { "convert upper camel case to lower hyphen" in { - val res1 = convertUpperCamelToLowerHyphen("HelloWorld") - val res2 = convertUpperCamelToLowerHyphen(" Hello World ") + val res1 = Formatter.convertUpperCamelToLowerHyphen("HelloWorld") + val res2 = Formatter.convertUpperCamelToLowerHyphen(" Hello World ") val expected = "hello-world" @@ -23,7 +23,7 @@ class UtilsSpec extends AnyWordSpecBase { SomeEnum.values.toSeq .map(_.toString) - .map(convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected + .map(Formatter.convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected } } } diff --git a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala index a88d8c6..38e3a13 100644 --- a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala +++ b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala @@ -27,7 +27,7 @@ class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils { RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), ) - val jwtVerifier = new JwtVerifier(verifierConfig) + val jwtVerifier = new JwtVerifierSpec(verifierConfig) val jwtIssuer = new JwtIssuer(issuerConfig) "JsoniterConversion" should { diff --git a/oath/macros/src/main/scala/io/oath/OathEnumMacro.scala b/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala similarity index 93% rename from oath/macros/src/main/scala/io/oath/OathEnumMacro.scala rename to oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala index b593f75..d364492 100644 --- a/oath/macros/src/main/scala/io/oath/OathEnumMacro.scala +++ b/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala @@ -1,4 +1,4 @@ -package io.oath +package io.oath.macros import scala.quoted.* diff --git a/oath/macros/src/test/scala/io/oath/OathEnum.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnum.scala similarity index 60% rename from oath/macros/src/test/scala/io/oath/OathEnum.scala rename to oath/macros/src/test/scala/io/oath/macros/OathEnum.scala index 01736c2..f890ac3 100644 --- a/oath/macros/src/test/scala/io/oath/OathEnum.scala +++ b/oath/macros/src/test/scala/io/oath/macros/OathEnum.scala @@ -1,4 +1,5 @@ -package io.oath +package io.oath.macros -object OathEnum: +object OathEnum { inline def apply[A]: Set[A] = OathEnumMacro.enumValues[A].toSet +} diff --git a/oath/macros/src/test/scala/io/oath/OathEnumMacroSpec.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala similarity index 80% rename from oath/macros/src/test/scala/io/oath/OathEnumMacroSpec.scala rename to oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala index 5dc6fdd..474358e 100644 --- a/oath/macros/src/test/scala/io/oath/OathEnumMacroSpec.scala +++ b/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala @@ -1,15 +1,18 @@ -package io.oath +package io.oath.macros import org.scalatest.matchers.should import org.scalatest.wordspec.AnyWordSpec -class OathEnumMacroSpec extends AnyWordSpec with should.Matchers: +class OathEnumMacroSpec extends AnyWordSpec with should.Matchers { - enum Foo: + enum Foo { case Foo1, Foo2, Foo3, Foo4 + } - "OathEnumMacros" should: - - "discover all children directories (enum values) in a Sum type" in: + "OathEnumMacros" should { + "discover all children directories (enum values) in a Sum type" in { val fooChildren = OathEnum[Foo] fooChildren should contain theSameElementsAs Set(Foo.Foo1, Foo.Foo2, Foo.Foo3, Foo.Foo4) + } + } +} From a6e49d9ca104abf81bb2b950c940f4fc75cc219e Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sat, 14 Sep 2024 23:53:30 +0100 Subject: [PATCH 07/15] feat: test progress JwtVerifier --- .../test/scala/io/oath/JwtVerifierSpec.scala | 8 +- .../test/scala/io/oath/JwtVerifierSpec.scala | 314 ++++++++++-------- .../src/test/scala/io/oath/NestedHeader.scala | 2 +- .../test/scala/io/oath/NestedPayload.scala | 2 +- 4 files changed, 182 insertions(+), 144 deletions(-) diff --git a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala index 5f9dea2..5fce31d 100644 --- a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -281,19 +281,19 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified.left.value shouldBe JwtVerifyError - .VerificationError("JWT Token is empty.") + .VerificationError("JWTVerifier failed with an empty token.") verifiedH.left.value shouldBe JwtVerifyError - .VerificationError("JWT Token is empty.") + .VerificationError("JWTVerifier failed with an empty token.") verifiedP.left.value shouldBe JwtVerifyError - .VerificationError("JWT Token is empty.") + .VerificationError("JWTVerifier failed with an empty token.") verifiedHP.left.value shouldBe JwtVerifyError - .VerificationError("JWT Token is empty.") + .VerificationError("JWTVerifier failed with an empty token.") } } } diff --git a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala index d9d7b42..e169b6e 100644 --- a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -139,143 +139,181 @@ class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) } -// "fail to decode a token with header if exception raised in decoder" in { -// val jwtVerifier = JwtVerifier(defaultConfig) -// val token = JWT -// .create() -// .sign(defaultConfig.algorithm) -// -// val verified = jwtVerifier.verifyJwt[SimpleHeader](token.toTokenH) -// -// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) -// } -// -// "fail to decode a token with payload if exception raised in decoder" in { -// val jwtVerifier = JwtVerifier(defaultConfig) -// val token = JWT -// .create() -// .sign(defaultConfig.algorithm) -// -// val verified = jwtVerifier.verifyJwt[SimplePayload](token.toTokenP) -// -// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) -// } -// -// "fail to decode a token with header & payload if exception raised in decoder" in { -// val jwtVerifier = JwtVerifier(defaultConfig) -// val token = JWT -// .create() -// .sign(defaultConfig.algorithm) -// -// val verified = -// jwtVerifier.verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) -// -// verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) -// } -// -// "fail to verify token with VerificationError when provided with claims are not meet criteria" in { -// val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) -// val jwtVerifier = JwtVerifier(config) -// val token = JWT -// .create() -// .sign(config.algorithm) -// -// val verified = jwtVerifier.verifyJwt(token.toToken) -// -// verified.left.value shouldEqual JwtVerifyError.VerificationError( -// "JwtVerifier failed with JWTVerificationException", -// Some(new JWTVerificationException("The Claim 'iss' is not present in the JWT.")), -// ) -// } -// -// "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { -// (config: JwtVerifierConfig) => -// val jwtVerifier = JwtVerifier(config.copy(algorithm = null)) -// val token = JWT -// .create() -// .sign(config.algorithm) -// -// val verified = jwtVerifier.verifyJwt(token.toToken) -// -// verified.left.value shouldBe JwtVerifyError.VerificationError( -// "JwtVerifier failed with IllegalArgumentException", -// Some(new IllegalArgumentException("The Algorithm cannot be null.")), -// ) -// } -// -// "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { -// (config: JwtVerifierConfig) => -// val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"))) -// val token = JWT -// .create() -// .sign(config.algorithm) -// -// val verified = jwtVerifier.verifyJwt(token.toToken) -// -// verified.left.value shouldEqual -// JwtVerifyError -// .VerificationError( -// "JwtVerifier failed with verification error", -// Some(new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected.")), -// ) -// } -// -// "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { -// (config: JwtVerifierConfig) => -// val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"))) -// val algorithm = Algorithm.HMAC256("secret1") -// val token = JWT -// .create() -// .sign(algorithm) -// -// val verified = jwtVerifier.verifyJwt(token.toToken) -// -// verified.left.value shouldEqual -// JwtVerifyError -// .VerificationError( -// "JwtVerifier failed with SignatureVerificationException", -// null, -// ) -// } -// -// "fail to verify token with TokenExpired when JWT expires" in { -// val jwtVerifier = JwtVerifier(defaultConfig) -// val expiresAt = getInstantNowSeconds.minusSeconds(1) -// val token = JWT -// .create() -// .withExpiresAt(expiresAt) -// .sign(defaultConfig.algorithm) -// -// val verified = jwtVerifier.verifyJwt(token.toToken) -// -// verified.left.value shouldEqual -// JwtVerifyError -// .VerificationError(s"The Token has expired on $expiresAt.", null) -// } -// -// "fail to verify an empty string token" in { -// val jwtVerifier = JwtVerifier(defaultConfig) -// val token = "" -// val verified = jwtVerifier.verifyJwt(token.toToken) -// val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) -// val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) -// val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) -// -// verified.left.value shouldBe -// JwtVerifyError -// .VerificationError("JWT Token is empty.") -// -// verifiedH.left.value shouldBe -// JwtVerifyError -// .VerificationError("JWT Token is empty.") -// -// verifiedP.left.value shouldBe -// JwtVerifyError -// .VerificationError("JWT Token is empty.") -// -// verifiedHP.left.value shouldBe -// JwtVerifyError -// .VerificationError("JWT Token is empty.") -// } + "fail to decode a token with header if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = JWT + .create() + .sign(defaultConfig.algorithm) + + val decodingError = jwtVerifier + .verifyJwt[SimpleHeader](token.toTokenH) + .left + .value + .asInstanceOf[JwtVerifyError.DecodingError] + + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } + + "fail to decode a token with payload if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = JWT + .create() + .sign(defaultConfig.algorithm) + + val decodingError = jwtVerifier + .verifyJwt[SimplePayload](token.toTokenP) + .left + .value + .asInstanceOf[JwtVerifyError.DecodingError] + + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } + + "fail to decode a token with header & payload if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = JWT + .create() + .sign(defaultConfig.algorithm) + + val decodingError = + jwtVerifier + .verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) + .left + .value + .asInstanceOf[JwtVerifyError.DecodingError] + + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } + + "fail to verify token with VerificationError when provided with claims are not meet criteria" in { + val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) + val jwtVerifier = JwtVerifier(config) + val token = JWT + .create() + .sign(config.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[JWTVerificationException] + verificationError.underlying.value.getMessage shouldBe "The Claim 'iss' is not present in the JWT." + } + + "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = null)) + val token = JWT + .create() + .sign(config.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[IllegalArgumentException] + verificationError.underlying.value.getMessage shouldBe "The Algorithm cannot be null." + } + + "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"))) + val token = JWT + .create() + .sign(config.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[AlgorithmMismatchException] + verificationError.underlying.value.getMessage shouldBe "The provided Algorithm doesn't match the one defined in the JWT's Header." + } + + "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"))) + val algorithm = Algorithm.HMAC256("secret1") + val token = JWT + .create() + .sign(algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[SignatureVerificationException] + verificationError.underlying.value.getMessage shouldBe "The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256" + } + + "fail to verify token with TokenExpired when JWT expires" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val expiresAt = getInstantNowSeconds.minusSeconds(1) + val token = JWT + .create() + .withExpiresAt(expiresAt) + .sign(defaultConfig.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[TokenExpiredException] + verificationError.underlying.value.getMessage shouldBe s"The Token has expired on $expiresAt." + } + + "fail to verify an empty string token" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = "" + val verified = jwtVerifier.verifyJwt(token.toToken) + val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) + val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + + val verificationError = verified.left.value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JWTVerifier failed with an empty token." + verificationError.underlying shouldBe empty + + val verificationErrorH = verifiedH.left.value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationErrorH.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorH.underlying shouldBe empty + + val verificationErrorP = verifiedP.left.value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationErrorP.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorP.underlying shouldBe empty + + val verificationErrorHP = verifiedHP.left.value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationErrorHP.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorHP.underlying shouldBe empty + } } } diff --git a/oath/core/src/test/scala/io/oath/NestedHeader.scala b/oath/core/src/test/scala/io/oath/NestedHeader.scala index 1771650..53edb21 100644 --- a/oath/core/src/test/scala/io/oath/NestedHeader.scala +++ b/oath/core/src/test/scala/io/oath/NestedHeader.scala @@ -22,7 +22,7 @@ object NestedHeader { given ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces - given ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") + given failWithBoomDecoder: ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") given claimsEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces diff --git a/oath/core/src/test/scala/io/oath/NestedPayload.scala b/oath/core/src/test/scala/io/oath/NestedPayload.scala index 3783fb9..ed3710d 100644 --- a/oath/core/src/test/scala/io/oath/NestedPayload.scala +++ b/oath/core/src/test/scala/io/oath/NestedPayload.scala @@ -22,7 +22,7 @@ object NestedPayload { given ClaimsEncoder[SimplePayload] = simplePayload => simplePayload.asJson.noSpaces - given ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") + given failWithBoomDecoder: ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") given claimsEncoder: ClaimsEncoder[NestedPayload] = nestedPayload => nestedPayload.asJson.noSpaces From b60309636cfb17ca04513c9178df8ca7c55157e6 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Wed, 30 Oct 2024 00:23:22 +0000 Subject: [PATCH 08/15] feat: more changes --- build.sbt | 2 +- .../src/main/scala/io/oath/circe/syntax.scala | 7 +-- .../src/main/scala/io/oath/test/Main.scala | 2 +- .../test/scala/io/oath/OathIssuerSpec.scala | 2 +- .../io/oath/config/AlgorithmLoaderSpec.scala | 2 +- .../io/oath/config/JwtIssuerLoaderSpec.scala | 12 ++--- .../src/main/scala/io/oath/JwtIssuer.scala | 1 + .../src/main/scala/io/oath/JwtVerifier.scala | 8 +-- .../src/main/scala/io/oath/OathIssuer.scala | 8 ++- .../src/main/scala/io/oath/OathManager.scala | 8 ++- .../src/main/scala/io/oath/OathVerifier.scala | 8 ++- .../io/oath/config/AlgorithmLoader.scala | 10 ++-- .../io/oath/config/JwtIssuerConfig.scala | 6 +-- .../io/oath/config/JwtVerifierConfig.scala | 8 +-- .../main/scala/io/oath/config/package.scala | 19 ++++--- .../core/src/main/scala/io/oath/package.scala | 50 ++++--------------- .../scala/io/oath/syntax/JwtBuilderOps.scala | 36 +++++++++++++ .../scala/io/oath/syntax/JwtClaimsOps.scala | 7 ++- .../scala/io/oath/syntax/JwtTokenOps.scala | 8 +-- .../io/oath/syntax/RegisteredClaimsOps.scala | 2 +- .../main/scala/io/oath/syntax/internal.scala | 3 ++ .../src/main/scala/io/oath/utils/Base64.scala | 2 +- .../main/scala/io/oath/utils/Formatter.scala | 2 +- .../test/scala/io/oath/JwtManagerSpec.scala | 47 +++++++++++++++++ .../test/scala/io/oath/OathIssuerSpec.scala | 25 ++++++++++ .../io/oath/config/AlgorithmLoaderSpec.scala | 2 +- .../io/oath/config/JwtIssuerLoaderSpec.scala | 12 ++--- .../scala/io/oath/macros/EnumExtensions.scala | 10 ++++ .../src/main/scala/io/oath/macros/Main.scala | 10 ++++ .../scala/io/oath/macros/OathEnumMacro.scala | 20 +++++--- .../test/scala/io/oath/macros/OathEnum.scala | 5 -- .../io/oath/macros/OathEnumMacroSpec.scala | 2 +- 32 files changed, 230 insertions(+), 116 deletions(-) create mode 100644 oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala create mode 100644 oath/core/src/main/scala/io/oath/syntax/internal.scala create mode 100644 oath/core/src/test/scala/io/oath/JwtManagerSpec.scala create mode 100644 oath/core/src/test/scala/io/oath/OathIssuerSpec.scala create mode 100644 oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala create mode 100644 oath/macros/src/main/scala/io/oath/macros/Main.scala delete mode 100644 oath/macros/src/test/scala/io/oath/macros/OathEnum.scala diff --git a/build.sbt b/build.sbt index 7d2b4a3..c7b5cc2 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import org.typelevel.sbt.gha.Permissions Global / onChangedBuildSource := ReloadOnSourceChanges -ThisBuild / scalaVersion := "3.4.2" +ThisBuild / scalaVersion := "3.3.4" ThisBuild / organization := "io.github.scala-jwt" ThisBuild / organizationName := "oath" ThisBuild / organizationHomepage := Some(url("https://github.com/scala-jwt/oath")) diff --git a/oath/circe/src/main/scala/io/oath/circe/syntax.scala b/oath/circe/src/main/scala/io/oath/circe/syntax.scala index 6bf1eeb..05e95d9 100644 --- a/oath/circe/src/main/scala/io/oath/circe/syntax.scala +++ b/oath/circe/src/main/scala/io/oath/circe/syntax.scala @@ -6,10 +6,10 @@ import io.oath.JwtVerifyError import io.oath.json.{ClaimsCodec, ClaimsDecoder, ClaimsEncoder} object syntax { - extension [P](encoder: Encoder[P]) inline def convert: ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces + extension [P](encoder: Encoder[P]) def convert: ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces extension [P](decoder: Decoder[P]) - inline def convert: ClaimsDecoder[P] = + def convert: ClaimsDecoder[P] = json => parser .parse(json) @@ -22,10 +22,11 @@ object syntax { ) extension [P](codec: Codec[P]) - inline def convertCodec: ClaimsCodec[P] = new ClaimsCodec[P]: + def convertCodec: ClaimsCodec[P] = new ClaimsCodec[P] { override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = codec.asInstanceOf[Decoder[P]].convert.decode(token) override def encode(data: P): String = codec.asInstanceOf[Encoder[P]].convert.encode(data) + } } diff --git a/oath/core-test/src/main/scala/io/oath/test/Main.scala b/oath/core-test/src/main/scala/io/oath/test/Main.scala index 9af009b..9830e8d 100644 --- a/oath/core-test/src/main/scala/io/oath/test/Main.scala +++ b/oath/core-test/src/main/scala/io/oath/test/Main.scala @@ -43,7 +43,7 @@ object Main extends App, Arbitraries { val defaultConfig = arbJwtVerifierConfig.arbitrary.sample.get - val jwtVerifier = new JwtVerifier(defaultConfig) + val jwtVerifier = JwtVerifier(defaultConfig) val (_, builder) = setRegisteredClaims(JWT.create(), defaultConfig) diff --git a/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala index 99ef530..1aee2d0 100644 --- a/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala @@ -6,7 +6,7 @@ class OathIssuerSpec extends AnyWordSpecBase { "OathIssuer" should { "create jwt token issuers" in { - inline def oathIssuer = OathIssuer.createOrFail[OathToken] + def oathIssuer = OathIssuer.createOrFail[OathToken] val accessTokenIssuer: JIssuer[OathToken.AccessToken.type] = oathIssuer.as(OathToken.AccessToken) val refreshTokenIssuer: JIssuer[OathToken.RefreshToken.type] = oathIssuer.as(OathToken.RefreshToken) diff --git a/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala index 7e874cd..c488ed9 100644 --- a/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -5,7 +5,7 @@ import com.typesafe.config.ConfigFactory import io.oath.test.* class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { - inline val AlgorithmConfigLocation = "algorithm" + val AlgorithmConfigLocation = "algorithm" "AlgorithmLoader" should { "load none encryption algorithm config" in forAll { (issuer: String) => diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index 396a38a..6ac763a 100644 --- a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -7,12 +7,12 @@ import scala.concurrent.duration.DurationInt class JwtIssuerLoaderSpec extends AnyWordSpecBase { - inline val configFile = "issuer" - inline val DefaultTokenConfigLocation = "default-token" - inline val TokenConfigLocation = "token" - inline val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" - inline val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" - inline val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + val configFile = "issuer" + val DefaultTokenConfigLocation = "default-token" + val TokenConfigLocation = "token" + val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" + val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" + val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" "IssuerLoader" should { "load default-token issuer config values from configuration file" in { diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index 68b46c3..a89cd8d 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -4,6 +4,7 @@ import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.{JWT, JWTCreator} import io.oath.config.* import io.oath.json.ClaimsEncoder +import io.oath.syntax.internal.* import java.time.temporal.ChronoUnit import java.time.{Clock, Instant} diff --git a/oath/core/src/main/scala/io/oath/JwtVerifier.scala b/oath/core/src/main/scala/io/oath/JwtVerifier.scala index e48b87b..65d251d 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifier.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifier.scala @@ -45,7 +45,7 @@ object JwtVerifier { ) .build() - inline private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = + private def getRegisteredClaims(decodedJWT: DecodedJWT): RegisteredClaims = RegisteredClaims( iss = decodedJWT.getOptionIssuer, sub = decodedJWT.getOptionSubject, @@ -56,19 +56,19 @@ object JwtVerifier { jti = decodedJWT.getOptionJwtID, ) - inline private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = + private def validateToken(token: String): Either[JwtVerifyError.VerificationError, String] = Option(token) .filter(_.nonEmpty) .toRight(JwtVerifyError.VerificationError("JWTVerifier failed with an empty token.")) - inline private def safeDecode[T]( + private def safeDecode[T]( decodedObject: => Either[JwtVerifyError.DecodingError, T] ): Either[JwtVerifyError.DecodingError, T] = allCatch .withTry(decodedObject) .fold(error => Left(JwtVerifyError.DecodingError(error.getMessage, error)), identity) - inline private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = + private def verify(token: String): Either[JwtVerifyError, DecodedJWT] = allCatch .withTry(jwtVerifier.verify(token)) .toEither diff --git a/oath/core/src/main/scala/io/oath/OathIssuer.scala b/oath/core/src/main/scala/io/oath/OathIssuer.scala index 4d6011b..435aa10 100644 --- a/oath/core/src/main/scala/io/oath/OathIssuer.scala +++ b/oath/core/src/main/scala/io/oath/OathIssuer.scala @@ -17,13 +17,17 @@ object OathIssuer { def as[S <: A](tokenType: S): JIssuer[S] = mapping(tokenType) } - inline def none[A]: OathIssuer[A] = + def none[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathIssuer[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtIssuer(JwtIssuerConfig.none()) }.toMap .pipe(mapping => new JavaJwtOathIssuer(mapping)) - inline def createOrFail[A]: OathIssuer[A] = + def createOrFail[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathIssuer[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtIssuerConfig.loadOrThrowOath(tokenConfig).pipe(JwtIssuer(_)) }.toMap diff --git a/oath/core/src/main/scala/io/oath/OathManager.scala b/oath/core/src/main/scala/io/oath/OathManager.scala index 69aa360..0d91fba 100644 --- a/oath/core/src/main/scala/io/oath/OathManager.scala +++ b/oath/core/src/main/scala/io/oath/OathManager.scala @@ -17,13 +17,17 @@ object OathManager { def as[S <: A](tokenType: S): JManager[S] = mapping(tokenType) } - inline def none[A]: OathManager[A] = + def none[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathManager[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtManager(JwtManagerConfig.none()) }.toMap .pipe(mapping => new JavaJwtOathManager(mapping)) - inline def createOrFail[A]: OathManager[A] = + def createOrFail[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathManager[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtManagerConfig.loadOrThrowOath(tokenConfig).pipe(JwtManager(_)) }.toMap diff --git a/oath/core/src/main/scala/io/oath/OathVerifier.scala b/oath/core/src/main/scala/io/oath/OathVerifier.scala index 85436d6..4598234 100644 --- a/oath/core/src/main/scala/io/oath/OathVerifier.scala +++ b/oath/core/src/main/scala/io/oath/OathVerifier.scala @@ -18,13 +18,17 @@ object OathVerifier { def as[S <: A](tokenType: S): JVerifier[S] = mapping(tokenType) } - inline def none[A]: OathVerifier[A] = + def none[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathVerifier[A] = getEnumValues[A].map { case (tokenType, _) => tokenType -> JwtVerifier(JwtVerifierConfig.none()) }.toMap .pipe(mapping => new JavaJwtOathVerifier(mapping)) - inline def createOrFail[A]: OathVerifier[A] = + def createOrFail[A](using + m: scala.deriving.Mirror.SumOf[A] + ): OathVerifier[A] = getEnumValues[A].map { case (tokenType, tokenConfig) => tokenType -> JwtVerifierConfig.loadOrThrowOath(tokenConfig).pipe(JwtVerifier(_)) }.toMap diff --git a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala index 9e9e106..988ab93 100644 --- a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala +++ b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala @@ -12,11 +12,11 @@ import scala.util.Using import scala.util.chaining.* object AlgorithmLoader { - inline private val SecretKeyConfigValue = "secret-key" - inline private val PrivateKeyPemPathConfigValue = "private-key-pem-path" - inline private val PublicKeyPemPathConfigValue = "public-key-pem-path" - inline private val RSAKeyFactoryInstance = "RSA" - inline private val ECKeyFactoryInstance = "EC" + private val SecretKeyConfigValue = "secret-key" + private val PrivateKeyPemPathConfigValue = "private-key-pem-path" + private val PublicKeyPemPathConfigValue = "public-key-pem-path" + private val RSAKeyFactoryInstance = "RSA" + private val ECKeyFactoryInstance = "EC" private val RSAKeyFactory = KeyFactory.getInstance(RSAKeyFactoryInstance) private val ECKeyFactory = KeyFactory.getInstance(ECKeyFactoryInstance) diff --git a/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala index 6c4b6ec..0467f6f 100644 --- a/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtIssuerConfig.scala @@ -9,9 +9,9 @@ import scala.concurrent.duration.FiniteDuration final case class JwtIssuerConfig(algorithm: Algorithm, registered: RegisteredConfig) object JwtIssuerConfig { - inline private val IssuerConfigLocation = "issuer" - inline private val AlgorithmConfigLocation = "algorithm" - inline private val RegisteredConfigLocation = "registered" + private val IssuerConfigLocation = "issuer" + private val AlgorithmConfigLocation = "algorithm" + private val RegisteredConfigLocation = "registered" final case class RegisteredConfig( issuerClaim: Option[String] = None, diff --git a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala index 17eda57..ee1f914 100644 --- a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala @@ -13,10 +13,10 @@ final case class JwtVerifierConfig( ) object JwtVerifierConfig { - inline private val VerifierConfigLocation = "verifier" - inline private val AlgorithmConfigLocation = "algorithm" - inline private val ProvidedWithConfigLocation = "provided-with" - inline private val LeewayWindowConfigLocation = "leeway-window" + private val VerifierConfigLocation = "verifier" + private val AlgorithmConfigLocation = "algorithm" + private val ProvidedWithConfigLocation = "provided-with" + private val LeewayWindowConfigLocation = "leeway-window" final case class ProvidedWithConfig( issuerClaim: Option[String] = None, diff --git a/oath/core/src/main/scala/io/oath/config/package.scala b/oath/core/src/main/scala/io/oath/config/package.scala index e380bd9..3fe748e 100644 --- a/oath/core/src/main/scala/io/oath/config/package.scala +++ b/oath/core/src/main/scala/io/oath/config/package.scala @@ -8,16 +8,15 @@ import scala.jdk.DurationConverters.* import scala.util.chaining.* import scala.util.control.Exception.allCatch -inline private[config] val OathLocation = "oath" -private[config] val rootConfig = ConfigFactory.load().getConfig(OathLocation) +private[config] val OathLocation = "oath" +private[config] val rootConfig = ConfigFactory.load().getConfig(OathLocation) extension (config: Config) { - inline private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { - case _: ConfigException.Missing => - default + private def ifMissingDefault[T](default: T): PartialFunction[Throwable, T] = { case _: ConfigException.Missing => + default } - inline private[config] def getMaybeNonEmptyString(path: String): Option[String] = + private[config] def getMaybeNonEmptyString(path: String): Option[String] = allCatch .withTry(Some(config.getString(path))) .recover(ifMissingDefault(Option.empty)) @@ -25,19 +24,19 @@ extension (config: Config) { .flatten .tap(value => if (value.exists(_.isEmpty)) throw new IllegalArgumentException(s"$path empty string not allowed.")) - inline private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = + private[config] def getMaybeFiniteDuration(path: String): Option[FiniteDuration] = allCatch .withTry(Some(config.getDuration(path).toScala)) .recover(ifMissingDefault(None)) .get - inline private[config] def getBooleanDefaultFalse(path: String): Boolean = + private[config] def getBooleanDefaultFalse(path: String): Boolean = allCatch .withTry(config.getBoolean(path)) .recover(ifMissingDefault(false)) .get - inline private[config] def getSeqNonEmptyString(path: String): Seq[String] = + private[config] def getSeqNonEmptyString(path: String): Seq[String] = allCatch .withTry(config.getStringList(path).asScala.toSeq) .recover(ifMissingDefault(Seq.empty)) @@ -47,7 +46,7 @@ extension (config: Config) { throw new IllegalArgumentException(s"$path empty string in the list not allowed.") ) - inline private[config] def getMaybeConfig(path: String): Option[Config] = + private[config] def getMaybeConfig(path: String): Option[Config] = allCatch .withTry(Some(config.getConfig(path))) .recover(ifMissingDefault(Option.empty)) diff --git a/oath/core/src/main/scala/io/oath/package.scala b/oath/core/src/main/scala/io/oath/package.scala index 0c3900b..51073ca 100644 --- a/oath/core/src/main/scala/io/oath/package.scala +++ b/oath/core/src/main/scala/io/oath/package.scala @@ -1,58 +1,28 @@ package io.oath -import com.auth0.jwt.JWTCreator.Builder import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.json.ClaimsEncoder import io.oath.macros.OathEnumMacro import io.oath.utils.Formatter import java.time.Instant import scala.jdk.CollectionConverters.CollectionHasAsScala -import scala.util.chaining.scalaUtilChainingOps -import scala.util.control.Exception.allCatch // TODO: Move to a file and test it properly -inline private def getEnumValues[A]: Set[(A, String)] = +inline def getEnumValues[A](using + m: scala.deriving.Mirror.SumOf[A] +): Set[(A, String)] = OathEnumMacro .enumValues[A] - .toSet .map(value => value -> Formatter.convertUpperCamelToLowerHyphen(value.toString)) // TODO: Move to file extension (decodedJWT: DecodedJWT) { - inline def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) - inline def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) - inline def getSeqAudience: Seq[String] = + def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) + def getOptionSubject: Option[String] = Option(decodedJWT.getSubject) + def getSeqAudience: Seq[String] = Option(decodedJWT.getAudience).map(_.asScala).toSeq.flatten - inline def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) - inline def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) - inline def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) - inline def getOptionJwtID: Option[String] = Option(decodedJWT.getId) -} - -// TODO: Move to file -extension (builder: Builder) { - private def safeEncode[T]( - claims: T, - toBuilder: String => Builder, - )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = - allCatch - .withTry( - claimsEncoder - .encode(claims) - .pipe(toBuilder) - ) - .toEither - .left - .map(error => JwtIssueError.EncodeError("Failed when trying to encode token", error)) - - inline def safeEncodeHeader[H](claims: H)(using - ClaimsEncoder[H] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withHeader) - - inline def safeEncodePayload[P](claims: P)(using - ClaimsEncoder[P] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withPayload) + def getOptionExpiresAt: Option[Instant] = Option(decodedJWT.getExpiresAt).map(_.toInstant) + def getOptionNotBefore: Option[Instant] = Option(decodedJWT.getNotBefore).map(_.toInstant) + def getOptionIssueAt: Option[Instant] = Option(decodedJWT.getIssuedAt).map(_.toInstant) + def getOptionJwtID: Option[String] = Option(decodedJWT.getId) } diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala new file mode 100644 index 0000000..0653c2d --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala @@ -0,0 +1,36 @@ +package io.oath.syntax + +import com.auth0.jwt.JWTCreator.Builder +import io.oath.JwtIssueError +import io.oath.json.ClaimsEncoder + +import scala.util.chaining.scalaUtilChainingOps +import scala.util.control.Exception.allCatch + +private[oath] trait JwtBuilderOps { + extension (builder: Builder) { + private def safeEncode[T]( + claims: T, + toBuilder: String => Builder, + )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = + allCatch + .withTry( + claimsEncoder + .encode(claims) + .pipe(toBuilder) + ) + .toEither + .left + .map(error => JwtIssueError.EncodeError("Failed when trying to encode token", error)) + + def safeEncodeHeader[H](claims: H)(using + ClaimsEncoder[H] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withHeader) + + def safeEncodePayload[P](claims: P)(using + ClaimsEncoder[P] + ): Either[JwtIssueError.EncodeError, Builder] = + safeEncode(claims, builder.withPayload) + } +} diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala index 4d4914e..9563837 100644 --- a/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala +++ b/oath/core/src/main/scala/io/oath/syntax/JwtClaimsOps.scala @@ -4,11 +4,10 @@ import io.oath.JwtClaims trait JwtClaimsOps { extension [A](value: A) - inline def toClaimsP: JwtClaims.ClaimsP[A] = JwtClaims.ClaimsP(value) - inline def toClaimsH: JwtClaims.ClaimsH[A] = JwtClaims.ClaimsH(value) + def toClaimsP: JwtClaims.ClaimsP[A] = JwtClaims.ClaimsP(value) + def toClaimsH: JwtClaims.ClaimsH[A] = JwtClaims.ClaimsH(value) - extension [A, B](value: (A, B)) - inline def toClaimsHP: JwtClaims.ClaimsHP[A, B] = JwtClaims.ClaimsHP(value._1, value._2) + extension [A, B](value: (A, B)) def toClaimsHP: JwtClaims.ClaimsHP[A, B] = JwtClaims.ClaimsHP(value._1, value._2) } object JwtClaimsOps extends JwtClaimsOps diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala index 87c68b0..b77f857 100644 --- a/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala +++ b/oath/core/src/main/scala/io/oath/syntax/JwtTokenOps.scala @@ -4,10 +4,10 @@ import io.oath.JwtToken trait JwtTokenOps { extension (value: String) { - inline def toToken: JwtToken.Token = JwtToken.Token(value) - inline def toTokenH: JwtToken.TokenH = JwtToken.TokenH(value) - inline def toTokenP: JwtToken.TokenP = JwtToken.TokenP(value) - inline def toTokenHP: JwtToken.TokenHP = JwtToken.TokenHP(value) + def toToken: JwtToken.Token = JwtToken.Token(value) + def toTokenH: JwtToken.TokenH = JwtToken.TokenH(value) + def toTokenP: JwtToken.TokenP = JwtToken.TokenP(value) + def toTokenHP: JwtToken.TokenHP = JwtToken.TokenHP(value) } } diff --git a/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala b/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala index ac2d604..19e6d6d 100644 --- a/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala +++ b/oath/core/src/main/scala/io/oath/syntax/RegisteredClaimsOps.scala @@ -3,7 +3,7 @@ package io.oath.syntax import io.oath.{JwtClaims, RegisteredClaims} trait RegisteredClaimsOps { - extension (value: RegisteredClaims) inline def toClaims: JwtClaims.Claims = JwtClaims.Claims(value) + extension (value: RegisteredClaims) def toClaims: JwtClaims.Claims = JwtClaims.Claims(value) } object RegisteredClaimsOps extends RegisteredClaimsOps diff --git a/oath/core/src/main/scala/io/oath/syntax/internal.scala b/oath/core/src/main/scala/io/oath/syntax/internal.scala new file mode 100644 index 0000000..08864fb --- /dev/null +++ b/oath/core/src/main/scala/io/oath/syntax/internal.scala @@ -0,0 +1,3 @@ +package io.oath.syntax + +private[oath] object internal extends JwtBuilderOps diff --git a/oath/core/src/main/scala/io/oath/utils/Base64.scala b/oath/core/src/main/scala/io/oath/utils/Base64.scala index 859dd82..0733c12 100644 --- a/oath/core/src/main/scala/io/oath/utils/Base64.scala +++ b/oath/core/src/main/scala/io/oath/utils/Base64.scala @@ -9,7 +9,7 @@ import scala.util.control.Exception.allCatch private[oath] object Base64 { // TODO: not tested - inline def decodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = + def decodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = allCatch .withTry(new String(JBase64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) .toEither diff --git a/oath/core/src/main/scala/io/oath/utils/Formatter.scala b/oath/core/src/main/scala/io/oath/utils/Formatter.scala index 6b4c2cd..f45609d 100644 --- a/oath/core/src/main/scala/io/oath/utils/Formatter.scala +++ b/oath/core/src/main/scala/io/oath/utils/Formatter.scala @@ -3,6 +3,6 @@ package io.oath.utils private[oath] object Formatter { // TODO: not tested properly - inline def convertUpperCamelToLowerHyphen(str: String): String = + def convertUpperCamelToLowerHyphen(str: String): String = str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim } diff --git a/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala new file mode 100644 index 0000000..54ff34c --- /dev/null +++ b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala @@ -0,0 +1,47 @@ +package io.oath + +import io.oath.config.* +import io.oath.syntax.all.* +import io.oath.testkit.* + +class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting { + + "JwtManager" should { + "be able to issue and verify jwt tokens without claims" in forAll { (config: JwtManagerConfig) => + val jwtManager = JwtManager(config) + + val jwt = jwtManager.issueJwt().value + jwtManager.verifyJwt(jwt.token.toToken).value.registered shouldBe jwt.claims.registered + } + + "be able to issue and verify jwt tokens with header claims" in forAll { + (config: JwtManagerConfig, nestedHeader: NestedHeader) => + val jwtManager = JwtManager(config) + + val claims = nestedHeader.toClaimsH + val jwt = jwtManager.issueJwt(claims).value + jwtManager.verifyJwt[NestedHeader](jwt.token.toTokenH).value shouldBe claims + .copy(registered = jwt.claims.registered) + } + + "be able to issue and verify jwt tokens with payload claims" in forAll { + (config: JwtManagerConfig, nestedPayload: NestedPayload) => + val jwtManager = JwtManager(config) + + val claims = nestedPayload.toClaimsP + val jwt = jwtManager.issueJwt(claims).value + jwtManager.verifyJwt[NestedPayload](jwt.token.toTokenP).value shouldBe claims + .copy(registered = jwt.claims.registered) + } + + "be able to issue and verify jwt tokens with header & payload claims" in forAll { + (config: JwtManagerConfig, nestedHeader: NestedHeader, nestedPayload: NestedPayload) => + val jwtManager = JwtManager(config) + + val claims = (nestedHeader, nestedPayload).toClaimsHP + val jwt = jwtManager.issueJwt(claims).value + jwtManager.verifyJwt[NestedHeader, NestedPayload](jwt.token.toTokenHP).value shouldBe claims + .copy(registered = jwt.claims.registered) + } + } +} diff --git a/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala b/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala new file mode 100644 index 0000000..bccdadc --- /dev/null +++ b/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala @@ -0,0 +1,25 @@ +package io.oath + +import io.oath.OathIssuer.JIssuer +import io.oath.testkit.AnyWordSpecBase + +class OathIssuerSpec extends AnyWordSpecBase { + + "OathIssuer" should { + "create jwt token issuers" in { + val oathIssuer = OathIssuer.createOrFail[OathToken] + + val accessTokenIssuer: JIssuer[OathToken.AccessToken.type] = oathIssuer.as(OathToken.AccessToken) + val refreshTokenIssuer: JIssuer[OathToken.RefreshToken.type] = oathIssuer.as(OathToken.RefreshToken) + val activationEmailTokenIssuer: JIssuer[OathToken.ActivationEmailToken.type] = + oathIssuer.as(OathToken.ActivationEmailToken) + val forgotPasswordTokenIssuer: JIssuer[OathToken.ForgotPasswordToken.type] = + oathIssuer.as(OathToken.ForgotPasswordToken) + + accessTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("access-token") + refreshTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("refresh-token") + activationEmailTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("activation-email-token") + forgotPasswordTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("forgot-password-token") + } + } +} diff --git a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala index a06f261..12c1b0e 100644 --- a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -5,7 +5,7 @@ import com.typesafe.config.ConfigFactory import io.oath.testkit.* class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { - inline val AlgorithmConfigLocation = "algorithm" + val AlgorithmConfigLocation = "algorithm" "AlgorithmLoader" should { "load none encryption algorithm config" in forAll { (issuer: String) => diff --git a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index bfa94d7..8500626 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -7,12 +7,12 @@ import scala.concurrent.duration.DurationInt class JwtIssuerLoaderSpec extends AnyWordSpecBase { - inline val configFile = "issuer" - inline val DefaultTokenConfigLocation = "default-token" - inline val TokenConfigLocation = "token" - inline val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" - inline val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" - inline val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + val configFile = "issuer" + val DefaultTokenConfigLocation = "default-token" + val TokenConfigLocation = "token" + val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" + val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" + val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" "IssuerLoader" should { "load default-token issuer config values from configuration file" in { diff --git a/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala b/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala new file mode 100644 index 0000000..6ca4ead --- /dev/null +++ b/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala @@ -0,0 +1,10 @@ +package io.oath.macros + +trait EnumExtension[E] { + def values: Set[E] +} + +object EnumExtension { + + inline def apply[E](using ) +} \ No newline at end of file diff --git a/oath/macros/src/main/scala/io/oath/macros/Main.scala b/oath/macros/src/main/scala/io/oath/macros/Main.scala new file mode 100644 index 0000000..4340aaf --- /dev/null +++ b/oath/macros/src/main/scala/io/oath/macros/Main.scala @@ -0,0 +1,10 @@ +package io.oath.macros + +object Main extends App { + + enum Color { + case Red, Green, Blue + } + + val values = +} diff --git a/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala b/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala index d364492..d3dd2c6 100644 --- a/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala +++ b/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala @@ -1,11 +1,17 @@ package io.oath.macros -import scala.quoted.* +object OathEnumMacro { -object OathEnumMacro: - inline def enumValues[E]: Array[E] = ${ enumValuesImpl[E] } + inline def enumValues[T](using + m: scala.deriving.Mirror.SumOf[T] + ): Set[T] = + allInstances[m.MirroredElemTypes, m.MirroredType].toSet - def enumValuesImpl[E: Type](using Quotes): Expr[Array[E]] = - import quotes.reflect.* - val companion = Ref(TypeTree.of[E].symbol.companionModule) - Select.unique(companion, "values").asExprOf[Array[E]] + inline def allInstances[ET <: Tuple, T]: List[T] = + import scala.compiletime.* + + inline erasedValue[ET] match + case _: EmptyTuple => Nil + case _: (t *: ts) => + summonInline[ValueOf[t]].value.asInstanceOf[T] :: allInstances[ts, T] +} diff --git a/oath/macros/src/test/scala/io/oath/macros/OathEnum.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnum.scala deleted file mode 100644 index f890ac3..0000000 --- a/oath/macros/src/test/scala/io/oath/macros/OathEnum.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.oath.macros - -object OathEnum { - inline def apply[A]: Set[A] = OathEnumMacro.enumValues[A].toSet -} diff --git a/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala index 474358e..288930a 100644 --- a/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala +++ b/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala @@ -11,7 +11,7 @@ class OathEnumMacroSpec extends AnyWordSpec with should.Matchers { "OathEnumMacros" should { "discover all children directories (enum values) in a Sum type" in { - val fooChildren = OathEnum[Foo] + val fooChildren = OathEnumMacro.enumValues[Foo] fooChildren should contain theSameElementsAs Set(Foo.Foo1, Foo.Foo2, Foo.Foo3, Foo.Foo4) } } From 428bdda46f8d5c2113c5a78c31f97afcbb5ed867 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Wed, 30 Oct 2024 00:25:59 +0000 Subject: [PATCH 09/15] update steward message --- .scala-steward.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scala-steward.conf b/.scala-steward.conf index 1216a3e..32bad4b 100644 --- a/.scala-steward.conf +++ b/.scala-steward.conf @@ -1,2 +1,2 @@ -commits.message = "chore: update ${artifactName} from ${currentVersion} to ${nextVersion}" +commits.message = "chore(deps): update ${artifactName} from ${currentVersion} to ${nextVersion}" From d25a6a870863868f91b9b5f4598abc27c8bcb646 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 8 Dec 2024 01:27:50 +0000 Subject: [PATCH 10/15] update to scala 3 --- build.sbt | 19 +- .../io/oath/circe/CirceConversionSpec.scala | 73 ++--- .../scala/io/oath/test/AnyWordSpecBase.scala | 7 - .../main/scala/io/oath/test/Arbitraries.scala | 139 -------- .../main/scala/io/oath/test/ClockHelper.scala | 10 - .../main/scala/io/oath/test/CodecUtils.scala | 10 - .../src/main/scala/io/oath/test/Main.scala | 55 ---- .../scala/io/oath/test/NestedHeader.scala | 38 --- .../scala/io/oath/test/NestedPayload.scala | 33 -- .../main/scala/io/oath/test/OathToken.scala | 5 - .../io/oath/test/PropertyBasedTesting.scala | 11 - .../main/scala/io/oath/test/TestData.scala | 6 - .../src/test/resources/algorithm-es256.conf | 5 - .../src/test/resources/algorithm-es384.conf | 5 - .../src/test/resources/algorithm-es512.conf | 5 - .../src/test/resources/algorithm-hsxxx.conf | 4 - .../src/test/resources/algorithm-none.conf | 3 - .../src/test/resources/algorithm-rsxxx.conf | 5 - .../test/resources/algorithm-unsupported.conf | 3 - oath/core-test/src/test/resources/issuer.conf | 66 ---- .../core-test/src/test/resources/manager.conf | 31 -- .../src/test/resources/reference.conf | 112 ------- .../src/test/resources/verifier.conf | 71 ----- .../test/scala/io/oath/JwtIssuerSpec.scala | 193 ----------- .../test/scala/io/oath/JwtManagerSpec.scala | 47 --- .../test/scala/io/oath/JwtVerifierSpec.scala | 299 ------------------ .../test/scala/io/oath/OathIssuerSpec.scala | 24 -- .../src/test/scala/io/oath/UtilsSpec.scala | 29 -- .../io/oath/config/AlgorithmLoaderSpec.scala | 101 ------ .../io/oath/config/JwtIssuerLoaderSpec.scala | 77 ----- .../io/oath/config/JwtManagerLoaderSpec.scala | 37 --- .../oath/config/JwtVerifierLoaderSpec.scala | 78 ----- .../src/test/secrets/es256-private.pem | 5 - .../src/test/secrets/es256-public.pem | 4 - .../src/test/secrets/es384-private.pem | 6 - .../src/test/secrets/es384-public.pem | 5 - .../src/test/secrets/es512-private.pem | 7 - .../src/test/secrets/es512-public.pem | 6 - .../src/test/secrets/rsa-private.pem | 28 -- .../core-test/src/test/secrets/rsa-public.pem | 9 - .../src/main/scala/io/oath/JwtClaims.scala | 2 +- .../src/main/scala/io/oath/OathIssuer.scala | 19 +- .../src/main/scala/io/oath/OathManager.scala | 23 +- .../src/main/scala/io/oath/OathVerifier.scala | 25 +- .../core/src/main/scala/io/oath/package.scala | 10 - .../src/main/scala/io/oath/utils/Base64.scala | 1 - .../main/scala/io/oath/utils/Formatter.scala | 8 - .../test/scala/io/oath/JwtIssuerSpec.scala | 2 +- .../test/scala/io/oath/JwtManagerSpec.scala | 2 +- .../test/scala/io/oath/JwtVerifierSpec.scala | 2 +- .../test/scala/io/oath/OathIssuerSpec.scala | 4 +- .../test/scala/io/oath/OathManagerSpec.scala | 5 +- .../src/test/scala/io/oath/OathToken.scala | 4 +- .../test/scala/io/oath/OathVerifierSpec.scala | 6 +- .../io/oath/config/AlgorithmLoaderSpec.scala | 2 +- .../io/oath/config/JwtIssuerLoaderSpec.scala | 2 +- .../io/oath/config/JwtManagerLoaderSpec.scala | 2 +- .../oath/config/JwtVerifierLoaderSpec.scala | 2 +- ...yWordSpecBase.scala => WordSpecBase.scala} | 2 +- .../test/scala/io/oath/utils/Base64Spec.scala | 14 + .../scala/io/oath/utils/FormatterSpec.scala | 29 -- .../JsoniterConversionSpec.scala | 13 +- .../scala/io/oath/macros/EnumExtensions.scala | 10 - .../src/main/scala/io/oath/macros/Main.scala | 10 - .../main/scala/io/oath/macros/OathEnum.scala | 32 ++ .../scala/io/oath/macros/OathEnumMacro.scala | 17 - .../io/oath/macros/OathEnumMacroSpec.scala | 18 -- .../scala/io/oath/macros/OathEnumSpec.scala | 91 ++++++ project/Dependencies.scala | 4 +- project/plugins.sbt | 2 +- 70 files changed, 234 insertions(+), 1800 deletions(-) delete mode 100644 oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/Main.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/OathToken.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala delete mode 100644 oath/core-test/src/main/scala/io/oath/test/TestData.scala delete mode 100644 oath/core-test/src/test/resources/algorithm-es256.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-es384.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-es512.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-hsxxx.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-none.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-rsxxx.conf delete mode 100644 oath/core-test/src/test/resources/algorithm-unsupported.conf delete mode 100644 oath/core-test/src/test/resources/issuer.conf delete mode 100644 oath/core-test/src/test/resources/manager.conf delete mode 100644 oath/core-test/src/test/resources/reference.conf delete mode 100644 oath/core-test/src/test/resources/verifier.conf delete mode 100644 oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/UtilsSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala delete mode 100644 oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala delete mode 100644 oath/core-test/src/test/secrets/es256-private.pem delete mode 100644 oath/core-test/src/test/secrets/es256-public.pem delete mode 100644 oath/core-test/src/test/secrets/es384-private.pem delete mode 100644 oath/core-test/src/test/secrets/es384-public.pem delete mode 100644 oath/core-test/src/test/secrets/es512-private.pem delete mode 100644 oath/core-test/src/test/secrets/es512-public.pem delete mode 100644 oath/core-test/src/test/secrets/rsa-private.pem delete mode 100644 oath/core-test/src/test/secrets/rsa-public.pem delete mode 100644 oath/core/src/main/scala/io/oath/utils/Formatter.scala rename oath/{core-test => core}/src/test/scala/io/oath/OathManagerSpec.scala (92%) rename oath/{core-test => core}/src/test/scala/io/oath/OathVerifierSpec.scala (92%) rename oath/core/src/test/scala/io/oath/testkit/{AnyWordSpecBase.scala => WordSpecBase.scala} (61%) create mode 100644 oath/core/src/test/scala/io/oath/utils/Base64Spec.scala delete mode 100644 oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala delete mode 100644 oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala delete mode 100644 oath/macros/src/main/scala/io/oath/macros/Main.scala create mode 100644 oath/macros/src/main/scala/io/oath/macros/OathEnum.scala delete mode 100644 oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala delete mode 100644 oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala create mode 100644 oath/macros/src/test/scala/io/oath/macros/OathEnumSpec.scala diff --git a/build.sbt b/build.sbt index c7b5cc2..81758c2 100644 --- a/build.sbt +++ b/build.sbt @@ -107,24 +107,10 @@ lazy val oathCore = createOathModule(Some("core")) ) ) -lazy val oathCoreTest = createOathModule(Some("core-test")) - .enablePlugins(NoPublishPlugin) - .dependsOn(oathCore) - .settings( - libraryDependencies ++= Seq( - Dependencies.scalaTest, - Dependencies.scalaTestPlusScalaCheck, - Dependencies.scalacheck, - Dependencies.circeCore, - Dependencies.circeGeneric, - Dependencies.circeParser, - ) - ) - lazy val oathCirce = createOathModule(Some("circe")) .dependsOn( oathCore, - oathCoreTest % Test, + oathCore % "test->test", ) .settings( libraryDependencies ++= Seq( @@ -137,7 +123,7 @@ lazy val oathCirce = createOathModule(Some("circe")) lazy val oathJsoniterScala = createOathModule(Some("jsoniter-scala")) .dependsOn( oathCore, - oathCoreTest % Test, + oathCore % "test->test", ) .settings( libraryDependencies ++= Seq( @@ -149,7 +135,6 @@ lazy val oathJsoniterScala = createOathModule(Some("jsoniter-scala")) lazy val oathModules: Seq[ProjectReference] = Seq( oathMacros, oathCore, - oathCoreTest, oathCirce, oathJsoniterScala, ) diff --git a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala index 9204bde..d388a12 100644 --- a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala +++ b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala @@ -1,21 +1,23 @@ package io.oath.circe +import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.oath.* import io.oath.circe.conversion.given import io.oath.config.JwtIssuerConfig.RegisteredConfig import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} import io.oath.config.* -import io.oath.syntax.* -import io.oath.testkit.AnyWordSpecBase -import io.oath.utils.CodecUtils +import io.oath.json.ClaimsDecoder +import io.oath.syntax.all.* +import io.oath.testkit.CodecHelper.unsafeParseJsonToJavaMap +import io.oath.testkit.* +import org.typelevel.jawn.ParseException -class CirceConversionSpec extends AnyWordSpecBase, CodecUtils { +class CirceConversionSpec extends WordSpecBase { val verifierConfig = JwtVerifierConfig( Algorithm.HMAC256("secret"), - None, ProvidedWithConfig(None, None, Nil), LeewayWindowConfig(None, None, None, None), ) @@ -23,12 +25,11 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils { val issuerConfig = JwtIssuerConfig( Algorithm.HMAC256("secret"), - None, RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), ) - val jwtVerifier = new JwtVerifierSpec(verifierConfig) - val jwtIssuer = new JwtIssuer(issuerConfig) + val jwtVerifier = JwtVerifier(verifierConfig) + val jwtIssuer = JwtIssuer(issuerConfig) "CirceConversion" should { "convert circe (encoders & decoders) to claims (encoders & decoders)" in { @@ -38,33 +39,33 @@ class CirceConversionSpec extends AnyWordSpecBase, CodecUtils { claims.payload shouldBe bar } - // - // "convert circe (codec) to claims (encoders & decoders)" in { - // val foo = Foo("foo", 10) - // val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("issuer")))).value - // val claims = jwtVerifier.verifyJwt[Foo](jwt.token.toTokenP).value - // - // claims.payload shouldBe foo - // } - // - // "convert circe decoder to claims decoder and get error" in { - // val fooJson = """{"name":"Hello","age":"not number"}""" - // val jwt = JWT - // .create() - // .withPayload(unsafeParseJsonToJavaMap(fooJson)) - // .sign(Algorithm.HMAC256("secret")) - // val claims = jwtVerifier.verifyJwt[Foo](jwt.toTokenP) - // - // claims.left.value shouldBe JwtVerifyError.DecodingError("DecodingFailure at .age: Int", null) - // } - // - // "convert circe decoder to claims decoder and get error when format is incorrect" in { - // val fooJson = """{"name":,}""" - // - // summon[ClaimsDecoder[Foo]].decode(fooJson).left.value shouldEqual JwtVerifyError.DecodingError( - // "expected json value got ',}' (line 1, column 9)", - // ParseException("expected json value got ',}' (line 1, column 9)", 8, 1, 9), - // ) - // } + + "convert circe (codec) to claims (encoders & decoders)" in { + val foo = Foo("foo", 10) + val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(foo, RegisteredClaims.empty.copy(iss = Some("issuer")))).value + val claims = jwtVerifier.verifyJwt[Foo](jwt.token.toTokenP).value + + claims.payload shouldBe foo + } + + "convert circe decoder to claims decoder and get error" in { + val fooJson = """{"name":"Hello","age":"not number"}""" + val jwt = JWT + .create() + .withPayload(unsafeParseJsonToJavaMap(fooJson)) + .sign(Algorithm.HMAC256("secret")) + val claims = jwtVerifier.verifyJwt[Foo](jwt.toTokenP) + + claims.left.value shouldBe JwtVerifyError.DecodingError("DecodingFailure at .age: Int", null) + } + + "convert circe decoder to claims decoder and get error when format is incorrect" in { + val fooJson = """{"name":,}""" + + summon[ClaimsDecoder[Foo]].decode(fooJson).left.value shouldEqual JwtVerifyError.DecodingError( + "expected json value got ',}' (line 1, column 9)", + ParseException("expected json value got ',}' (line 1, column 9)", 8, 1, 9), + ) + } } } diff --git a/oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala b/oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala deleted file mode 100644 index 419a962..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/AnyWordSpecBase.scala +++ /dev/null @@ -1,7 +0,0 @@ -package io.oath.test - -import org.scalatest.matchers.should -import org.scalatest.wordspec.AnyWordSpec -import org.scalatest.{EitherValues, OptionValues} - -abstract class AnyWordSpecBase extends AnyWordSpec, should.Matchers, OptionValues, EitherValues diff --git a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala b/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala deleted file mode 100644 index 80e44d1..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/Arbitraries.scala +++ /dev/null @@ -1,139 +0,0 @@ -package io.oath.test - -import com.auth0.jwt.algorithms.Algorithm -import io.oath.RegisteredClaims -import io.oath.config.JwtIssuerConfig.RegisteredConfig -import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} -import io.oath.config.* -import io.oath.test.NestedHeader.SimpleHeader -import io.oath.test.NestedPayload.SimplePayload -import org.scalacheck.* - -import java.time.Instant -import scala.concurrent.duration.{Duration, DurationInt} - -trait Arbitraries { - private lazy val genPositiveFiniteDuration = Gen.posNum[Long].map(Duration.fromNanos) - private lazy val genPositiveFiniteDurationSeconds = Gen.posNum[Int].map(x => (x + 1).seconds) - - implicit val arbNonEmptyString: Arbitrary[String] = - Arbitrary( - Gen.nonEmptyListOf[Char](Gen.alphaChar).map(_.mkString) - ) - - implicit val arbInstant: Arbitrary[Instant] = - Arbitrary( - Gen.chooseNum(Long.MinValue, Long.MaxValue).map(Instant.ofEpochMilli) - ) - - implicit val arbJwtIssuerConfig: Arbitrary[JwtIssuerConfig] = - Arbitrary { - for { - issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) - subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) - audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) - includeJwtIdClaim <- Arbitrary.arbitrary[Boolean] - includeIssueAtClaim <- Arbitrary.arbitrary[Boolean] - expiresAtOffset <- Gen.option(genPositiveFiniteDuration) - notBeforeOffset <- Gen.option(genPositiveFiniteDuration) - registered = RegisteredConfig( - issuerClaim, - subjectClaim, - audienceClaims, - includeJwtIdClaim, - includeIssueAtClaim, - expiresAtOffset, - notBeforeOffset, - ) - } yield JwtIssuerConfig(Algorithm.none(), registered) - } - - implicit val arbJwtVerifierConfig: Arbitrary[JwtVerifierConfig] = - Arbitrary { - for { - issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) - subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) - audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) - leeway <- Gen.option(genPositiveFiniteDurationSeconds) - issuedAt <- Gen.option(genPositiveFiniteDurationSeconds) - expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) - notBefore <- Gen.option(genPositiveFiniteDurationSeconds) - leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBefore) - providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) - } yield JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) - } - - implicit val arbJwtManagerConfig: Arbitrary[JwtManagerConfig] = - Arbitrary { - for { - issuerClaim <- Gen.option(arbNonEmptyString.arbitrary) - subjectClaim <- Gen.option(arbNonEmptyString.arbitrary) - audienceClaims <- Gen.listOf(arbNonEmptyString.arbitrary) - includeJwtIdClaim <- Arbitrary.arbitrary[Boolean] - includeIssueAtClaim <- Arbitrary.arbitrary[Boolean] - expiresAtOffset <- Gen.option(genPositiveFiniteDurationSeconds) - notBeforeOffset <- Gen.option(genPositiveFiniteDurationSeconds) - leeway <- Gen.option(genPositiveFiniteDurationSeconds) - issuedAt <- Gen.option(genPositiveFiniteDurationSeconds) - expiresAt <- Gen.option(genPositiveFiniteDurationSeconds) - leewayWindow = LeewayWindowConfig(leeway, issuedAt, expiresAt, notBeforeOffset.map(_.plus(1.second))) - providedWith = ProvidedWithConfig(issuerClaim, subjectClaim, audienceClaims) - registered = RegisteredConfig( - issuerClaim, - subjectClaim, - audienceClaims, - includeJwtIdClaim, - includeIssueAtClaim, - expiresAtOffset, - notBeforeOffset, - ) - verifier = JwtVerifierConfig(Algorithm.none(), providedWith, leewayWindow) - issuer = JwtIssuerConfig(Algorithm.none(), registered) - } yield JwtManagerConfig(issuer, verifier) - } - - implicit val arbRegisteredClaims: Arbitrary[RegisteredClaims] = - Arbitrary { - for { - iss <- Gen.option(arbNonEmptyString.arbitrary) - sub <- Gen.option(arbNonEmptyString.arbitrary) - aud <- Gen.listOf(arbNonEmptyString.arbitrary) - exp <- Gen.option(arbInstant.arbitrary) - nbf <- Gen.option(arbInstant.arbitrary) - iat <- Gen.option(arbInstant.arbitrary) - jti <- Gen.option(arbNonEmptyString.arbitrary) - } yield RegisteredClaims(iss, sub, aud, exp, nbf, iat, jti) - } - - implicit val arbSimplePayload: Arbitrary[SimplePayload] = - Arbitrary { - for { - name <- Gen.alphaStr - data <- Gen.listOf(Gen.alphaStr) - } yield SimplePayload(name, data) - } - - implicit val arbSimpleHeader: Arbitrary[SimpleHeader] = - Arbitrary { - for { - name <- Gen.alphaStr - data <- Gen.listOf(Gen.alphaStr) - } yield SimpleHeader(name, data) - } - - implicit val arbNestedPayload: Arbitrary[NestedPayload] = - Arbitrary { - for { - name <- Gen.alphaStr - mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimplePayload.arbitrary.map((str, _)))) - } yield NestedPayload(name, mapping) - } - - implicit val arbNestedHeader: Arbitrary[NestedHeader] = - Arbitrary { - for { - name <- Gen.alphaStr - mapping <- Gen.mapOf(Gen.alphaStr.flatMap(str => arbSimpleHeader.arbitrary.map((str, _)))) - } yield NestedHeader(name, mapping) - } -} diff --git a/oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala b/oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala deleted file mode 100644 index 833c68c..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/ClockHelper.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.oath.test - -import java.time.temporal.ChronoUnit -import java.time.{Clock, Instant, ZoneId} - -trait ClockHelper { - def getInstantNowSeconds: Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) - - def getFixedClock(time: Instant): Clock = Clock.fixed(time, ZoneId.of("UTC")) -} diff --git a/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala b/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala deleted file mode 100644 index 60ff175..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/CodecUtils.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.oath.test - -import com.fasterxml.jackson.databind.ObjectMapper - -trait CodecUtils { - private val mapper = new ObjectMapper() - - def unsafeParseJsonToJavaMap(json: String): java.util.Map[String, Object] = - mapper.readValue(json, classOf[java.util.HashMap[String, Object]]) -} diff --git a/oath/core-test/src/main/scala/io/oath/test/Main.scala b/oath/core-test/src/main/scala/io/oath/test/Main.scala deleted file mode 100644 index 9830e8d..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/Main.scala +++ /dev/null @@ -1,55 +0,0 @@ -package io.oath.test - -import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.config.JwtVerifierConfig -import io.oath.syntax.all.* -import io.oath.{JwtVerifier, RegisteredClaims} - -import java.time.Instant -import java.time.temporal.ChronoUnit -import scala.util.chaining.scalaUtilChainingOps - -object Main extends App, Arbitraries { - - def getInstantNowSeconds: Instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) - - def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig) = { - val now = getInstantNowSeconds - val leeway = config.leewayWindow.leeway.map(leeway => now.plusSeconds(leeway.toSeconds - 1)) - val expiresAt = config.leewayWindow.expiresAt.map(expiresAt => now.plusSeconds(expiresAt.toSeconds - 1)) - val notBefore = config.leewayWindow.notBefore.map(notBefore => now.plusSeconds(notBefore.toSeconds - 1)) - val issueAt = config.leewayWindow.issuedAt.map(issueAt => now.plusSeconds(issueAt.toSeconds - 1)) - - val registeredClaims = RegisteredClaims( - config.providedWith.issuerClaim, - config.providedWith.subjectClaim, - config.providedWith.audienceClaims, - expiresAt orElse leeway, - notBefore orElse leeway, - issueAt orElse leeway, - None, - ) - - val builderWithRegistered = builder - .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) - .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) - .tap(builder => builder.withAudience(registeredClaims.aud*)) - .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) - .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) - .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) - - registeredClaims -> builderWithRegistered - } - - val defaultConfig = arbJwtVerifierConfig.arbitrary.sample.get - - val jwtVerifier = JwtVerifier(defaultConfig) - - val (_, builder) = setRegisteredClaims(JWT.create(), defaultConfig) - - val token = builder.sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - println(verified) -} diff --git a/oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala b/oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala deleted file mode 100644 index b67267b..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/NestedHeader.scala +++ /dev/null @@ -1,38 +0,0 @@ -package io.oath.test - -import io.circe.* -import io.circe.generic.semiauto.* -import io.circe.parser.* -import io.circe.syntax.* -import io.oath.* -import io.oath.json.* -import io.oath.test.NestedHeader.SimpleHeader - -final case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) - -object NestedHeader { - final case class SimpleHeader(name: String, data: List[String]) - - given simpleHeaderCirceEncoder: Encoder[SimpleHeader] = deriveEncoder[SimpleHeader] - - given simpleHeaderCirceDecoder: Decoder[SimpleHeader] = deriveDecoder[SimpleHeader] - - given nestedHeaderCirceEncoder: Encoder[NestedHeader] = deriveEncoder[NestedHeader] - - given nestedHeaderCirceDecoder: Decoder[NestedHeader] = deriveDecoder[NestedHeader] - - given simpleHeaderEncoder: ClaimsEncoder[SimpleHeader] = simpleHeader => simpleHeader.asJson.noSpaces - - given simpleHeaderDecoder: ClaimsDecoder[SimpleHeader] = _ => throw new RuntimeException("Boom") - - given nestedHeaderEncoder: ClaimsEncoder[NestedHeader] = nestedHeader => nestedHeader.asJson.noSpaces - - given nestedHeaderDecoder: ClaimsDecoder[NestedHeader] = nestedHeaderJson => - parse(nestedHeaderJson).left - .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) - .flatMap( - _.as[NestedHeader].left.map(decodingFailure => - JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) - ) - ) -} diff --git a/oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala b/oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala deleted file mode 100644 index 58de79c..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/NestedPayload.scala +++ /dev/null @@ -1,33 +0,0 @@ -package io.oath.test - -import io.circe.generic.semiauto.* -import io.circe.parser.* -import io.circe.syntax.* -import io.circe.{Decoder, Encoder} -import io.oath.* -import io.oath.json.* -import io.oath.test.NestedPayload.SimplePayload - -final case class NestedPayload(name: String, mapping: Map[String, SimplePayload]) - -object NestedPayload: - final case class SimplePayload(name: String, data: List[String]) - - given simplePayloadCirceEncoder: Encoder[SimplePayload] = deriveEncoder[SimplePayload] - given simplePayloadCirceDecoder: Decoder[SimplePayload] = deriveDecoder[SimplePayload] - - given nestedPayloadCirceEncoder: Encoder[NestedPayload] = deriveEncoder[NestedPayload] - given nestedPayloadCirceDecoder: Decoder[NestedPayload] = deriveDecoder[NestedPayload] - - given simplePayloadEncoder: ClaimsEncoder[SimplePayload] = simplePayload => simplePayload.asJson.noSpaces - given simplePayloadDecoder: ClaimsDecoder[SimplePayload] = _ => throw new RuntimeException("Boom") - - given nestedPayloadEncoder: ClaimsEncoder[NestedPayload] = nestedPayload => nestedPayload.asJson.noSpaces - given nestedPayloadDecoder: ClaimsDecoder[NestedPayload] = nestedPayloadJson => - parse(nestedPayloadJson).left - .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) - .flatMap( - _.as[NestedPayload].left.map(decodingFailure => - JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) - ) - ) diff --git a/oath/core-test/src/main/scala/io/oath/test/OathToken.scala b/oath/core-test/src/main/scala/io/oath/test/OathToken.scala deleted file mode 100644 index 0bcadb6..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/OathToken.scala +++ /dev/null @@ -1,5 +0,0 @@ -package io.oath.test - -enum OathToken { - case AccessToken, RefreshToken, ActivationEmailToken, ForgotPasswordToken -} diff --git a/oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala b/oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala deleted file mode 100644 index bbfe022..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/PropertyBasedTesting.scala +++ /dev/null @@ -1,11 +0,0 @@ -package io.oath.test - -import org.scalactic.anyvals.PosInt -import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks - -trait PropertyBasedTesting extends ScalaCheckPropertyChecks, Arbitraries { - val minSuccessful = PosInt(25) - - override implicit val generatorDrivenConfig: PropertyCheckConfiguration = - PropertyCheckConfiguration(minSuccessful) -} diff --git a/oath/core-test/src/main/scala/io/oath/test/TestData.scala b/oath/core-test/src/main/scala/io/oath/test/TestData.scala deleted file mode 100644 index bc5d16e..0000000 --- a/oath/core-test/src/main/scala/io/oath/test/TestData.scala +++ /dev/null @@ -1,6 +0,0 @@ -package io.oath.test - -import com.auth0.jwt.JWTCreator -import io.oath.RegisteredClaims - -case class TestData(registeredClaims: RegisteredClaims, builder: JWTCreator.Builder) diff --git a/oath/core-test/src/test/resources/algorithm-es256.conf b/oath/core-test/src/test/resources/algorithm-es256.conf deleted file mode 100644 index c9b900c..0000000 --- a/oath/core-test/src/test/resources/algorithm-es256.conf +++ /dev/null @@ -1,5 +0,0 @@ -algorithm { - name = "ES256" - private-key-pem-path = "src/test/secrets/es256-private.pem" - public-key-pem-path = "src/test/secrets/es256-public.pem" -} diff --git a/oath/core-test/src/test/resources/algorithm-es384.conf b/oath/core-test/src/test/resources/algorithm-es384.conf deleted file mode 100644 index a210a2f..0000000 --- a/oath/core-test/src/test/resources/algorithm-es384.conf +++ /dev/null @@ -1,5 +0,0 @@ -algorithm { - name = "ES384" - private-key-pem-path = "src/test/secrets/es384-private.pem" - public-key-pem-path = "src/test/secrets/es384-public.pem" -} diff --git a/oath/core-test/src/test/resources/algorithm-es512.conf b/oath/core-test/src/test/resources/algorithm-es512.conf deleted file mode 100644 index eed5da0..0000000 --- a/oath/core-test/src/test/resources/algorithm-es512.conf +++ /dev/null @@ -1,5 +0,0 @@ -algorithm { - name = "ES512" - private-key-pem-path = "src/test/secrets/es512-private.pem" - public-key-pem-path = "src/test/secrets/es512-public.pem" -} diff --git a/oath/core-test/src/test/resources/algorithm-hsxxx.conf b/oath/core-test/src/test/resources/algorithm-hsxxx.conf deleted file mode 100644 index 9fe86a5..0000000 --- a/oath/core-test/src/test/resources/algorithm-hsxxx.conf +++ /dev/null @@ -1,4 +0,0 @@ -algorithm { - name = "HS256" - secret-key = "secret" -} diff --git a/oath/core-test/src/test/resources/algorithm-none.conf b/oath/core-test/src/test/resources/algorithm-none.conf deleted file mode 100644 index 46711e5..0000000 --- a/oath/core-test/src/test/resources/algorithm-none.conf +++ /dev/null @@ -1,3 +0,0 @@ -algorithm { - name = "NONE" -} diff --git a/oath/core-test/src/test/resources/algorithm-rsxxx.conf b/oath/core-test/src/test/resources/algorithm-rsxxx.conf deleted file mode 100644 index fdc48ed..0000000 --- a/oath/core-test/src/test/resources/algorithm-rsxxx.conf +++ /dev/null @@ -1,5 +0,0 @@ -algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-private.pem" - public-key-pem-path = "src/test/secrets/rsa-public.pem" -} diff --git a/oath/core-test/src/test/resources/algorithm-unsupported.conf b/oath/core-test/src/test/resources/algorithm-unsupported.conf deleted file mode 100644 index 208de17..0000000 --- a/oath/core-test/src/test/resources/algorithm-unsupported.conf +++ /dev/null @@ -1,3 +0,0 @@ -algorithm { - name = "Boom" -} diff --git a/oath/core-test/src/test/resources/issuer.conf b/oath/core-test/src/test/resources/issuer.conf deleted file mode 100644 index 87e1b16..0000000 --- a/oath/core-test/src/test/resources/issuer.conf +++ /dev/null @@ -1,66 +0,0 @@ -default-token { - algorithm { - name = "HS256" - secret-key = "secret" - } -} - -token { - algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-private.pem" - } - issuer { - registered { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = false - expires-at-offset = 1 day - not-before-offset = 1 minute - } - } -} - -without-private-key-token { - algorithm { - name = "RS256" - public-key-pem-path = "src/test/secrets/rsa-private.pem" - } - issuer { - registered { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = false - expires-at-offset = 1 day - not-before-offset = 1 minute - } - } -} - -invalid-token-empty-string { - algorithm { - name = "HS256" - secret-key = "secret" - } - issuer { - registered { - issuer-claim = "" - } - } -} - -invalid-token-wrong-type { - algorithm { - name = "HS256" - secret-key = "secret" - } - issuer { - registered { - not-before-offset = "" - } - } -} diff --git a/oath/core-test/src/test/resources/manager.conf b/oath/core-test/src/test/resources/manager.conf deleted file mode 100644 index 6f25c8c..0000000 --- a/oath/core-test/src/test/resources/manager.conf +++ /dev/null @@ -1,31 +0,0 @@ -token { - algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-private.pem" - public-key-pem-path = "src/test/secrets/rsa-public.pem" - } - issuer { - registered { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = false - expires-at-offset = 1 day - not-before-offset = 1 minute - } - } - verifier { - provided-with { - issuer-claim = ${token.issuer.registered.issuer-claim} - subject-claim = ${token.issuer.registered.subject-claim} - audience-claims = ${token.issuer.registered.audience-claims} - } - leeway-window { - leeway = 1 minute - issued-at = 4 minutes - expires-at = 3 minutes - not-before = 2 minutes - } - } -} diff --git a/oath/core-test/src/test/resources/reference.conf b/oath/core-test/src/test/resources/reference.conf deleted file mode 100644 index 5589b75..0000000 --- a/oath/core-test/src/test/resources/reference.conf +++ /dev/null @@ -1,112 +0,0 @@ -token { - algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-private.pem" - public-key-pem-path = "src/test/secrets/rsa-public.pem" - } - issuer { - registered { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = false - expires-at-offset = 1 day - not-before-offset = 1 minute - } - } - verifier { - provided-with { - issuer-claim = ${token.issuer.registered.issuer-claim} - subject-claim = ${token.issuer.registered.subject-claim} - audience-claims = ${token.issuer.registered.audience-claims} - } - leeway-window { - leeway = 1 minute - issued-at = 4 minutes - expires-at = 3 minutes - not-before = 2 minutes - } - } -} - -oath { - access-token { - algorithm { - name = "HS256" - secret-key = "secret" - } - issuer { - registered { - issuer-claim = "access-token" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - include-issued-at-claim = true - include-jwt-id-claim = true - expires-at-offset = 15 minutes - not-before-offset = 0 minute - } - } - verifier { - provided-with { - issuer-claim = ${oath.access-token.issuer.registered.issuer-claim} - subject-claim = ${oath.access-token.issuer.registered.subject-claim} - audience-claims = ${oath.access-token.issuer.registered.audience-claims} - } - leeway-window { - leeway = 1 minute - issued-at = 1 minute - expires-at = 1 minute - not-before = 1 minute - } - } - } - - refresh-token = ${oath.access-token} - refresh-token { - issuer { - registered { - issuer-claim = "refresh-token" - expires-at-offset = 6 hours - } - } - verifier { - provided-with { - issuer-claim = ${oath.refresh-token.issuer.registered.issuer-claim} - } - } - } - activation-email-token = ${oath.access-token} - activation-email-token { - issuer { - registered { - issuer-claim = "activation-email-token" - expires-at-offset = 1 day - audience-claims = [] - } - } - verifier { - provided-with { - issuer-claim = ${oath.activation-email-token.issuer.registered.issuer-claim} - audience-claims = [] - } - } - } - - forgot-password-token = ${oath.access-token} - forgot-password-token { - issuer { - registered { - issuer-claim = "forgot-password-token" - expires-at-offset = 2 hours - audience-claims = [] - } - } - verifier { - provided-with { - issuer-claim = ${oath.forgot-password-token.issuer.registered.issuer-claim} - audience-claims = [] - } - } - } -} diff --git a/oath/core-test/src/test/resources/verifier.conf b/oath/core-test/src/test/resources/verifier.conf deleted file mode 100644 index b192465..0000000 --- a/oath/core-test/src/test/resources/verifier.conf +++ /dev/null @@ -1,71 +0,0 @@ -default-token { - algorithm { - name = "HS256" - secret-key = "src/test/secrets/rsa-public.pem" - } -} - -token { - algorithm { - name = "RS256" - public-key-pem-path = "src/test/secrets/rsa-public.pem" - } - verifier { - provided-with { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - } - leeway-window { - leeway = 1 minute - issued-at = 4 minutes - expires-at = 3 minutes - not-before = 2 minutes - } - } -} - -without-public-key-token { - algorithm { - name = "RS256" - private-key-pem-path = "src/test/secrets/rsa-public.pem" - } - - verifier { - provided-with { - issuer-claim = "issuer" - subject-claim = "subject" - audience-claims = ["aud1", "aud2"] - } - leeway-window { - leeway = 1 minute - issued-at = 4 minutes - expires-at = 3 minutes - not-before = 2 minutes - } - } -} - -invalid-token-empty-string { - algorithm { - name = "HS256" - secret-key = "secret" - } - verifier { - provided-with { - issuer-claim = "" - } - } -} - -invalid-token-wrong-type { - algorithm { - name = "HS256" - secret-key = "secret" - } - verifier { - provided-with { - audience-claims = "" - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala deleted file mode 100644 index 672f919..0000000 --- a/oath/core-test/src/test/scala/io/oath/JwtIssuerSpec.scala +++ /dev/null @@ -1,193 +0,0 @@ -package io.oath - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import io.oath.config.* -import io.oath.syntax.all.* -import io.oath.test.NestedHeader.nestedHeaderDecoder -import io.oath.test.NestedPayload.nestedPayloadDecoder -import io.oath.test.* - -import scala.concurrent.duration.DurationInt -import scala.jdk.CollectionConverters.ListHasAsScala -import scala.util.Try -import scala.util.chaining.scalaUtilChainingOps - -class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { - - val jwtVerifier = JWT - .require(Algorithm.none()) - .acceptLeeway(5) - .build() - - "JwtIssuer" should { - "issue jwt tokens" when { - "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt().value - - val decodedJWT = jwtVerifier.verify(jwtClaims.token) - - Option(decodedJWT.getIssuer) shouldBe config.registered.issuerClaim - Option(decodedJWT.getSubject) shouldBe config.registered.subjectClaim - Option(decodedJWT.getAudience) - .map(_.asScala.to(Seq)) - .to(Seq) - .flatten shouldBe config.registered.audienceClaims - - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe Option.when(config.registered.includeIssueAtClaim)(now) - - if (config.registered.includeJwtIdClaim) - Option(decodedJWT.getId) should not be empty - else - Option(decodedJWT.getId) shouldBe empty - - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe config.registered.expiresAtOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe config.registered.notBeforeOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - } - - "issue token with predefine configure claims and ad-hoc registered claims" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(registeredClaims.toClaims).value - - val expectedIssuer = registeredClaims.iss orElse config.registered.issuerClaim - val expectedSubject = registeredClaims.sub orElse config.registered.subjectClaim - val expectedAudience = - if (registeredClaims.aud.nonEmpty) registeredClaims.aud else config.registered.audienceClaims - val expectedIssuedAt = registeredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now) - val expectedExpiredAt = - registeredClaims.exp orElse config.registered.expiresAtOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - val expectedNotBefore = - registeredClaims.nbf orElse config.registered.notBeforeOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - - jwtClaims.claims.registered.iss shouldBe expectedIssuer - jwtClaims.claims.registered.sub shouldBe expectedSubject - jwtClaims.claims.registered.aud shouldBe expectedAudience - jwtClaims.claims.registered.iat shouldBe expectedIssuedAt - jwtClaims.claims.registered.exp shouldBe expectedExpiredAt - jwtClaims.claims.registered.nbf shouldBe expectedNotBefore - - if (registeredClaims.jti.nonEmpty) - jwtClaims.claims.registered.jti shouldBe registeredClaims.jti - else if (config.registered.includeJwtIdClaim) - jwtClaims.claims.registered.jti should not be empty - else jwtClaims.claims.registered.jti shouldBe empty - } - - "issue token with only registered claims empty strings" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val adHocRegisteredClaims = - registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value - - val decodedJWT = jwtVerifier.verify(jwtClaims.token) - - Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss - Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub - Option(decodedJWT.getAudience) - .map(_.asScala.toSeq) - .toSeq - .flatten shouldBe jwtClaims.claims.registered.aud - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat - Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf - } - - "issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val adHocRegisteredClaims = - registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = new JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value - - val decodedJWT = jwtVerifier.verify(jwtClaims.token) - - Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss - Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub - Option(decodedJWT.getAudience) - .map(_.asScala.toSeq) - .toSeq - .flatten shouldBe jwtClaims.claims.registered.aud - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat - Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf - } - - "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(header.toClaimsH).value - - val result = jwtVerifier - .verify(jwt.token) - .pipe(_.getHeader) - .pipe(Base64.decodeToken) - .pipe(_.value) - .pipe(nestedHeaderDecoder.decode) - .value - - result shouldBe header - } - - "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value - - val result = jwtVerifier - .verify(jwt.token) - .pipe(_.getPayload) - .pipe(Base64.decodeToken) - .pipe(_.value) - .pipe(nestedPayloadDecoder.decode) - .value - - result shouldBe payload - } - - "issue token with header & payload claims" in forAll { - (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => - val jwtIssuer = new JwtIssuer(config) - val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value - - val (headerResult, payloadResult) = jwtVerifier - .verify(jwt.token) - .pipe(decodedJwt => - Base64.decodeToken(decodedJwt.getHeader).value -> Base64.decodeToken(decodedJwt.getPayload).value - ) - .pipe { case (headerJson, payloadJson) => - (nestedHeaderDecoder.decode(headerJson).value, nestedPayloadDecoder.decode(payloadJson).value) - } - - headerResult shouldBe header - payloadResult shouldBe payload - } - - "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { - (config: JwtIssuerConfig) => - val jwtIssuer = new JwtIssuer(config.copy(algorithm = null)) - val jwt = jwtIssuer.issueJwt() - - jwt.left.value shouldEqual JwtIssueError.SignError( - "Signing token failed", - new IllegalArgumentException("Algorithm cannot be null"), - ) - } - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala deleted file mode 100644 index 716ee63..0000000 --- a/oath/core-test/src/test/scala/io/oath/JwtManagerSpec.scala +++ /dev/null @@ -1,47 +0,0 @@ -package io.oath - -import io.oath.config.* -import io.oath.syntax.all.* -import io.oath.test.* - -class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting { - - "JwtManager" should { - "be able to issue and verify jwt tokens without claims" in forAll { (config: JwtManagerConfig) => - val jwtManager = new JwtManager(config) - - val jwt = jwtManager.issueJwt().value - jwtManager.verifyJwt(jwt.token.toToken).value.registered shouldBe jwt.claims.registered - } - - "be able to issue and verify jwt tokens with header claims" in forAll { - (config: JwtManagerConfig, nestedHeader: NestedHeader) => - val jwtManager = new JwtManager(config) - - val claims = nestedHeader.toClaimsH - val jwt = jwtManager.issueJwt(claims).value - jwtManager.verifyJwt[NestedHeader](jwt.token.toTokenH).value shouldBe claims - .copy(registered = jwt.claims.registered) - } - - "be able to issue and verify jwt tokens with payload claims" in forAll { - (config: JwtManagerConfig, nestedPayload: NestedPayload) => - val jwtManager = new JwtManager(config) - - val claims = nestedPayload.toClaimsP - val jwt = jwtManager.issueJwt(claims).value - jwtManager.verifyJwt[NestedPayload](jwt.token.toTokenP).value shouldBe claims - .copy(registered = jwt.claims.registered) - } - - "be able to issue and verify jwt tokens with header & payload claims" in forAll { - (config: JwtManagerConfig, nestedHeader: NestedHeader, nestedPayload: NestedPayload) => - val jwtManager = new JwtManager(config) - - val claims = (nestedHeader, nestedPayload).toClaimsHP - val jwt = jwtManager.issueJwt(claims).value - jwtManager.verifyJwt[NestedHeader, NestedPayload](jwt.token.toTokenHP).value shouldBe claims - .copy(registered = jwt.claims.registered) - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala deleted file mode 100644 index 5fce31d..0000000 --- a/oath/core-test/src/test/scala/io/oath/JwtVerifierSpec.scala +++ /dev/null @@ -1,299 +0,0 @@ -package io.oath - -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.* -import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.config.JwtVerifierConfig -import io.oath.config.JwtVerifierConfig.* -import io.oath.syntax.all.* -import io.oath.test.NestedHeader.{SimpleHeader, nestedHeaderEncoder} -import io.oath.test.NestedPayload.{SimplePayload, nestedPayloadEncoder} -import io.oath.test.* - -import scala.util.chaining.scalaUtilChainingOps - -class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper, CodecUtils { - - val defaultConfig = - JwtVerifierConfig( - Algorithm.none(), - ProvidedWithConfig(None, None, Nil), - LeewayWindowConfig(None, None, None, None), - ) - - def setRegisteredClaims(builder: JWTCreator.Builder, config: JwtVerifierConfig): TestData = { - val now = getInstantNowSeconds - val leeway = config.leewayWindow.leeway.map(leeway => now.plusSeconds(leeway.toSeconds - 1)) - val expiresAt = config.leewayWindow.expiresAt.map(expiresAt => now.plusSeconds(expiresAt.toSeconds - 1)) - val notBefore = config.leewayWindow.notBefore.map(notBefore => now.plusSeconds(notBefore.toSeconds - 1)) - val issueAt = config.leewayWindow.issuedAt.map(issueAt => now.plusSeconds(issueAt.toSeconds - 1)) - - val registeredClaims = RegisteredClaims( - config.providedWith.issuerClaim, - config.providedWith.subjectClaim, - config.providedWith.audienceClaims, - expiresAt orElse leeway, - notBefore orElse leeway, - issueAt orElse leeway, - None, - ) - - val builderWithRegistered = builder - .tap(builder => registeredClaims.iss.map(str => builder.withIssuer(str))) - .tap(builder => registeredClaims.sub.map(str => builder.withSubject(str))) - .tap(builder => builder.withAudience(registeredClaims.aud*)) - .tap(builder => registeredClaims.exp.map(builder.withExpiresAt)) - .tap(builder => registeredClaims.nbf.map(builder.withNotBefore)) - .tap(builder => registeredClaims.iat.map(builder.withIssuedAt)) - - TestData(registeredClaims, builderWithRegistered) - } - - "JwtVerifier" should { - "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder.sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken).value - - verified.registered shouldBe testData.registeredClaims - } - - "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withHeader(unsafeParseJsonToJavaMap(nestedHeaderEncoder.encode(nestedHeader))) - .sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - - verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) - } - - "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withPayload(unsafeParseJsonToJavaMap(nestedPayloadEncoder.encode(nestedPayload))) - .sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - - verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) - } - - "verify a token with header & payload" in forAll { - (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config) - - val testData = setRegisteredClaims(JWT.create(), config) - - val token = testData.builder - .withPayload(unsafeParseJsonToJavaMap(nestedPayloadEncoder.encode(nestedPayload))) - .withHeader(unsafeParseJsonToJavaMap(nestedHeaderEncoder.encode(nestedHeader))) - .sign(config.algorithm) - - val verified = - jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - - verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) - } - - "fail to decode a token with header" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val header = """{"name": "name"}""" - val token = JWT - .create() - .withHeader(unsafeParseJsonToJavaMap(header)) - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) - } - - "fail to decode a token with payload" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val payload = """{"name": "name"}""" - val token = JWT - .create() - .withPayload(unsafeParseJsonToJavaMap(payload)) - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) - } - - "fail to decode a token with header & payload" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val header = """{"name": "name"}""" - val token = JWT - .create() - .withHeader(unsafeParseJsonToJavaMap(header)) - .sign(defaultConfig.algorithm) - - val verified = - jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) - } - - "fail to decode a token with header if exception raised in decoder" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val token = JWT - .create() - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt[SimpleHeader](token.toTokenH) - - verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) - } - - "fail to decode a token with payload if exception raised in decoder" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val token = JWT - .create() - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt[SimplePayload](token.toTokenP) - - verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) - } - - "fail to decode a token with header & payload if exception raised in decoder" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val token = JWT - .create() - .sign(defaultConfig.algorithm) - - val verified = - jwtVerifier.verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) - - verified.left.value shouldEqual JwtVerifyError.DecodingError("Boom", null) - } - - "fail to verify token with VerificationError when provided with claims are not meet criteria" in { - val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) - val jwtVerifier = new JwtVerifierSpec(config) - - val token = JWT - .create() - .sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - verified.left.value shouldEqual JwtVerifyError.VerificationError( - "JwtVerifier failed with JWTVerificationException", - Some(new JWTVerificationException("The Claim 'iss' is not present in the JWT.")), - ) - } - - "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = null)) - - val token = JWT - .create() - .sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - verified.left.value shouldBe JwtVerifyError.VerificationError( - "JwtVerifier failed with IllegalArgumentException", - Some(new IllegalArgumentException("The Algorithm cannot be null.")), - ) - } - - "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = Algorithm.HMAC256("secret"))) - - val token = JWT - .create() - .sign(config.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - verified.left.value shouldEqual - JwtVerifyError - .VerificationError( - "JwtVerifier failed with verification error", - Some(new AlgorithmMismatchException("The Algorithm used to sign the JWT is not the one expected.")), - ) - } - - "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = new JwtVerifierSpec(config.copy(algorithm = Algorithm.HMAC256("secret2"))) - val algorithm = Algorithm.HMAC256("secret1") - val token = JWT - .create() - .sign(algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - verified.left.value shouldEqual - JwtVerifyError - .VerificationError( - "JwtVerifier failed with SignatureVerificationException", - null, - ) - } - - "fail to verify token with TokenExpired when JWT expires" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - - val expiresAt = getInstantNowSeconds.minusSeconds(1) - val token = JWT - .create() - .withExpiresAt(expiresAt) - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt(token.toToken) - - verified.left.value shouldEqual - JwtVerifyError - .VerificationError(s"The Token has expired on $expiresAt.", null) - } - - "fail to verify an empty string token" in { - val jwtVerifier = new JwtVerifierSpec(defaultConfig) - val token = "" - val verified = jwtVerifier.verifyJwt(token.toToken) - val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - - verified.left.value shouldBe - JwtVerifyError - .VerificationError("JWTVerifier failed with an empty token.") - - verifiedH.left.value shouldBe - JwtVerifyError - .VerificationError("JWTVerifier failed with an empty token.") - - verifiedP.left.value shouldBe - JwtVerifyError - .VerificationError("JWTVerifier failed with an empty token.") - - verifiedHP.left.value shouldBe - JwtVerifyError - .VerificationError("JWTVerifier failed with an empty token.") - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala b/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala deleted file mode 100644 index 1aee2d0..0000000 --- a/oath/core-test/src/test/scala/io/oath/OathIssuerSpec.scala +++ /dev/null @@ -1,24 +0,0 @@ -package io.oath - -import io.oath.test.* - -class OathIssuerSpec extends AnyWordSpecBase { - - "OathIssuer" should { - "create jwt token issuers" in { - def oathIssuer = OathIssuer.createOrFail[OathToken] - - val accessTokenIssuer: JIssuer[OathToken.AccessToken.type] = oathIssuer.as(OathToken.AccessToken) - val refreshTokenIssuer: JIssuer[OathToken.RefreshToken.type] = oathIssuer.as(OathToken.RefreshToken) - val activationEmailTokenIssuer: JIssuer[OathToken.ActivationEmailToken.type] = - oathIssuer.as(OathToken.ActivationEmailToken) - val forgotPasswordTokenIssuer: JIssuer[OathToken.ForgotPasswordToken.type] = - oathIssuer.as(OathToken.ForgotPasswordToken) - - accessTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("access-token") - refreshTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("refresh-token") - activationEmailTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("activation-email-token") - forgotPasswordTokenIssuer.issueJwt().value.claims.registered.iss shouldBe Some("forgot-password-token") - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/UtilsSpec.scala b/oath/core-test/src/test/scala/io/oath/UtilsSpec.scala deleted file mode 100644 index afcdd0c..0000000 --- a/oath/core-test/src/test/scala/io/oath/UtilsSpec.scala +++ /dev/null @@ -1,29 +0,0 @@ -package io.oath - -import io.oath.test.* - -class UtilsSpec extends AnyWordSpecBase { - - "FormatConversion" should { - "convert upper camel case to lower hyphen" in { - val res1 = convertUpperCamelToLowerHyphen("HelloWorld") - val res2 = convertUpperCamelToLowerHyphen(" Hello World ") - - val expected = "hello-world" - - res1 shouldBe expected - res2 shouldBe expected - } - - "convert scala enum string values to lower hyphen" in { - enum SomeEnum: - case firstEnum, SecondEnum, Third, ForthEnumValue - - val expected = Seq("first-enum", "second-enum", "third", "forth-enum-value") - - SomeEnum.values.toSeq - .map(_.toString) - .map(convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala deleted file mode 100644 index c488ed9..0000000 --- a/oath/core-test/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala +++ /dev/null @@ -1,101 +0,0 @@ -package io.oath.config - -import com.auth0.jwt.JWT -import com.typesafe.config.ConfigFactory -import io.oath.test.* - -class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { - val AlgorithmConfigLocation = "algorithm" - - "AlgorithmLoader" should { - "load none encryption algorithm config" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-none").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "none" - verifyingAlgorithm.getName shouldBe "none" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "load RSXXX encryption algorithm with secret key" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-rsxxx").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "RS256" - verifyingAlgorithm.getName shouldBe "RS256" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "load HSXXX encryption algorithm with secret key" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-hsxxx").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "HS256" - verifyingAlgorithm.getName shouldBe "HS256" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "load ES256 encryption algorithm with secret key" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-es256").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "ES256" - verifyingAlgorithm.getName shouldBe "ES256" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "load ES384 encryption algorithm with secret key" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-es384").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "ES384" - verifyingAlgorithm.getName shouldBe "ES384" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "load ES512 encryption algorithm with secret key" in forAll { (issuer: String) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-es512").getConfig(AlgorithmConfigLocation) - val issuingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = true) - val verifyingAlgorithm = AlgorithmLoader.loadOrThrow(algorithmScopedConfig, isIssuer = false) - - val token: String = JWT.create().withIssuer(issuer).sign(issuingAlgorithm) - val verifiedIssuer = JWT.require(verifyingAlgorithm).build().verify(token).getIssuer - - issuingAlgorithm.getName shouldBe "ES512" - verifyingAlgorithm.getName shouldBe "ES512" - verifiedIssuer shouldBe issuer - token should not be empty - } - - "fail to load unsupported algorithm type" in forAll { (bool: Boolean) => - val algorithmScopedConfig = ConfigFactory.load("algorithm-unsupported").getConfig(AlgorithmConfigLocation) - the[IllegalArgumentException] thrownBy AlgorithmLoader - .loadOrThrow(algorithmScopedConfig, bool) should have message "Unsupported signature algorithm: Boom" - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala deleted file mode 100644 index 6ac763a..0000000 --- a/oath/core-test/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ /dev/null @@ -1,77 +0,0 @@ -package io.oath.config - -import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.test.* - -import scala.concurrent.duration.DurationInt - -class JwtIssuerLoaderSpec extends AnyWordSpecBase { - - val configFile = "issuer" - val DefaultTokenConfigLocation = "default-token" - val TokenConfigLocation = "token" - val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" - val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" - val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" - - "IssuerLoader" should { - "load default-token issuer config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) - val config = JwtIssuerConfig.loadOrThrow(configLoader) - - config.registered.issuerClaim shouldBe None - config.registered.subjectClaim shouldBe None - config.registered.audienceClaims shouldBe Seq.empty - config.registered.includeIssueAtClaim shouldBe false - config.registered.includeJwtIdClaim shouldBe false - config.registered.expiresAtOffset shouldBe None - config.registered.notBeforeOffset shouldBe None - config.algorithm.getName shouldBe "HS256" - } - - "load token issuer config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) - val config = JwtIssuerConfig.loadOrThrow(configLoader) - - config.registered.issuerClaim shouldBe Some("issuer") - config.registered.subjectClaim shouldBe Some("subject") - config.registered.audienceClaims shouldBe Seq("aud1", "aud2") - config.registered.includeIssueAtClaim shouldBe true - config.registered.includeJwtIdClaim shouldBe false - config.registered.expiresAtOffset shouldBe Some(1.day) - config.registered.notBeforeOffset shouldBe Some(1.minute) - config.algorithm.getName shouldBe "RS256" - } - - "load token issuer config values from reference configuration file using location" in { - val config = JwtIssuerConfig.loadOrThrow(TokenConfigLocation) - - config.registered.issuerClaim shouldBe Some("issuer") - config.registered.subjectClaim shouldBe Some("subject") - config.registered.audienceClaims shouldBe Seq("aud1", "aud2") - config.registered.includeIssueAtClaim shouldBe true - config.registered.includeJwtIdClaim shouldBe false - config.registered.expiresAtOffset shouldBe Some(1.day) - config.registered.notBeforeOffset shouldBe Some(1.minute) - config.algorithm.getName shouldBe "RS256" - } - - "fail to load without-private-key-token issuer config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPrivateKeyTokenConfigLocation) - - the[ConfigException.Missing] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) - } - - "fail to load invalid-token-empty-string issuer config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) - - the[IllegalArgumentException] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) - } - - "fail to load invalid-token-wrong-type issuer config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) - - the[ConfigException.BadValue] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala deleted file mode 100644 index 7942906..0000000 --- a/oath/core-test/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala +++ /dev/null @@ -1,37 +0,0 @@ -package io.oath.config - -import com.typesafe.config.ConfigFactory -import io.oath.test.* - -import scala.concurrent.duration.DurationInt - -class JwtManagerLoaderSpec extends AnyWordSpecBase { - - val configFile = "manager" - val TokenConfigLocation = "token" - - "ManagerLoader" should { - "load default-token verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) - val config = JwtManagerConfig.loadOrThrow(configLoader) - - config.issuer.registered.issuerClaim shouldBe Some("issuer") - config.issuer.registered.subjectClaim shouldBe Some("subject") - config.issuer.registered.audienceClaims shouldBe Seq("aud1", "aud2") - config.issuer.registered.includeIssueAtClaim shouldBe true - config.issuer.registered.includeJwtIdClaim shouldBe false - config.issuer.registered.expiresAtOffset shouldBe Some(1.day) - config.issuer.registered.notBeforeOffset shouldBe Some(1.minute) - config.issuer.algorithm.getName shouldBe "RS256" - - config.verifier.providedWith.issuerClaim shouldBe Some("issuer") - config.verifier.providedWith.subjectClaim shouldBe Some("subject") - config.verifier.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") - config.verifier.leewayWindow.leeway shouldBe Some(1.minute) - config.verifier.leewayWindow.issuedAt shouldBe Some(4.minutes) - config.verifier.leewayWindow.expiresAt shouldBe Some(3.minutes) - config.verifier.leewayWindow.notBefore shouldBe Some(2.minutes) - config.verifier.algorithm.getName shouldBe "RS256" - } - } -} diff --git a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala deleted file mode 100644 index 729f963..0000000 --- a/oath/core-test/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala +++ /dev/null @@ -1,78 +0,0 @@ -package io.oath.config - -import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.test.* - -import scala.concurrent.duration.DurationInt - -class JwtVerifierLoaderSpec extends AnyWordSpecBase { - - val configFile = "verifier" - val DefaultTokenConfigLocation = "default-token" - val TokenConfigLocation = "token" - val WithoutPublicKeyTokenConfigLocation = "without-public-key-token" - val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" - val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" - - "VerifierLoader" should { - "load default-token verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(DefaultTokenConfigLocation) - val config = JwtVerifierConfig.loadOrThrow(configLoader) - - config.providedWith.issuerClaim shouldBe None - config.providedWith.subjectClaim shouldBe None - config.providedWith.audienceClaims shouldBe Seq.empty - config.leewayWindow.leeway shouldBe None - config.leewayWindow.expiresAt shouldBe None - config.leewayWindow.issuedAt shouldBe None - config.leewayWindow.expiresAt shouldBe None - config.leewayWindow.notBefore shouldBe None - config.algorithm.getName shouldBe "HS256" - } - - "load token verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(TokenConfigLocation) - val config = JwtVerifierConfig.loadOrThrow(configLoader) - - config.providedWith.issuerClaim shouldBe Some("issuer") - config.providedWith.subjectClaim shouldBe Some("subject") - config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") - config.leewayWindow.leeway shouldBe Some(1.minute) - config.leewayWindow.issuedAt shouldBe Some(4.minutes) - config.leewayWindow.expiresAt shouldBe Some(3.minutes) - config.leewayWindow.notBefore shouldBe Some(2.minutes) - config.algorithm.getName shouldBe "RS256" - } - - "load token verifier config values from reference configuration file using location" in { - val config = JwtVerifierConfig.loadOrThrow(TokenConfigLocation) - - config.providedWith.issuerClaim shouldBe Some("issuer") - config.providedWith.subjectClaim shouldBe Some("subject") - config.providedWith.audienceClaims shouldBe Seq("aud1", "aud2") - config.leewayWindow.leeway shouldBe Some(1.minute) - config.leewayWindow.issuedAt shouldBe Some(4.minutes) - config.leewayWindow.expiresAt shouldBe Some(3.minutes) - config.leewayWindow.notBefore shouldBe Some(2.minutes) - config.algorithm.getName shouldBe "RS256" - } - - "fail to load without-public-key-token verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(WithoutPublicKeyTokenConfigLocation) - - the[ConfigException.Missing] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) - } - - "fail to load invalid-token-empty-string verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenEmptyStringConfigLocation) - - the[IllegalArgumentException] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) - } - - "fail to load invalid-token-wrong-type verifier config values from configuration file" in { - val configLoader = ConfigFactory.load(configFile).getConfig(InvalidTokenWrongTypeConfigLocation) - - the[ConfigException.WrongType] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) - } - } -} diff --git a/oath/core-test/src/test/secrets/es256-private.pem b/oath/core-test/src/test/secrets/es256-private.pem deleted file mode 100644 index 756361a..0000000 --- a/oath/core-test/src/test/secrets/es256-private.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPGJGAm4X1fvBuC1z -SpO/4Izx6PXfNMaiKaS5RUkFqEGhRANCAARCBvmeksd3QGTrVs2eMrrfa7CYF+sX -sjyGg+Bo5mPKGH4Gs8M7oIvoP9pb/I85tdebtKlmiCZHAZE5w4DfJSV6 ------END PRIVATE KEY----- diff --git a/oath/core-test/src/test/secrets/es256-public.pem b/oath/core-test/src/test/secrets/es256-public.pem deleted file mode 100644 index 34401f7..0000000 --- a/oath/core-test/src/test/secrets/es256-public.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQgb5npLHd0Bk61bNnjK632uwmBfr -F7I8hoPgaOZjyhh+BrPDO6CL6D/aW/yPObXXm7SpZogmRwGROcOA3yUleg== ------END PUBLIC KEY----- diff --git a/oath/core-test/src/test/secrets/es384-private.pem b/oath/core-test/src/test/secrets/es384-private.pem deleted file mode 100644 index 9482bfa..0000000 --- a/oath/core-test/src/test/secrets/es384-private.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCVWQsOJHjKD0I4cXOY -Jm4G8i5c7IMhFbxFq57OUlrTVmND43dvvNW1oQ6i6NiXEQWhZANiAASezSGlAu4w -AaJe4676mQM0F/5slI+EkdptRJdfsQP9mNxe7RdzHgcSw7j/Wxa45nlnFnFrPPL4 -viJKOBRxMB1jjVA9my9PixxJGoB22qDQwFbP8ldmEp6abwdBsXNaePM= ------END PRIVATE KEY----- diff --git a/oath/core-test/src/test/secrets/es384-public.pem b/oath/core-test/src/test/secrets/es384-public.pem deleted file mode 100644 index 511596e..0000000 --- a/oath/core-test/src/test/secrets/es384-public.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PUBLIC KEY----- -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEns0hpQLuMAGiXuOu+pkDNBf+bJSPhJHa -bUSXX7ED/ZjcXu0Xcx4HEsO4/1sWuOZ5ZxZxazzy+L4iSjgUcTAdY41QPZsvT4sc -SRqAdtqg0MBWz/JXZhKemm8HQbFzWnjz ------END PUBLIC KEY----- diff --git a/oath/core-test/src/test/secrets/es512-private.pem b/oath/core-test/src/test/secrets/es512-private.pem deleted file mode 100644 index bde9098..0000000 --- a/oath/core-test/src/test/secrets/es512-private.pem +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIHtAgEAMBAGByqGSM49AgEGBSuBBAAjBIHVMIHSAgEBBEHzl1DpZSQJ8YhCbN/u -vo5SOu0BjDDX9Gub6zsBW6B2TxRzb5sBeQaWVscDUZha4Xr1HEWpVtua9+nEQU/9 -Aq9Pl6GBiQOBhgAEAJhvCa6S89ePqlLO6MRV9KQqHvdAITDAf/WRDcvCmfrrNuov -+j4gQXO12ohIukPCHM9rYms8Eqciz3gaxVTxZD4CAA8i2k9H6ew9iSh1qXa1kLxi -yzMBqmAmmg4u/SroD6OleG56SwZVbWx+KIINB6r/PQVciGX8FjwgR/mbLHotVZYD ------END PRIVATE KEY----- diff --git a/oath/core-test/src/test/secrets/es512-public.pem b/oath/core-test/src/test/secrets/es512-public.pem deleted file mode 100644 index 360209a..0000000 --- a/oath/core-test/src/test/secrets/es512-public.pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAmG8JrpLz14+qUs7oxFX0pCoe90Ah -MMB/9ZENy8KZ+us26i/6PiBBc7XaiEi6Q8Icz2tiazwSpyLPeBrFVPFkPgIADyLa -T0fp7D2JKHWpdrWQvGLLMwGqYCaaDi79KugPo6V4bnpLBlVtbH4ogg0Hqv89BVyI -ZfwWPCBH+Zssei1VlgM= ------END PUBLIC KEY----- diff --git a/oath/core-test/src/test/secrets/rsa-private.pem b/oath/core-test/src/test/secrets/rsa-private.pem deleted file mode 100644 index 1427e0d..0000000 --- a/oath/core-test/src/test/secrets/rsa-private.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4ZtdaIrd1BPIJ -tfnF0TjIK5inQAXZ3XlCrUlJdP+XHwIRxdv1FsN12XyMYO/6ymLmo9ryoQeIrsXB -XYqlET3zfAY+diwCb0HEsVvhisthwMU4gZQu6TYW2s9LnXZB5rVtcBK69hcSlA2k -ZudMZWxZcj0L7KMfO2rIvaHw/qaVOE9j0T257Z8Kp2CLF9MUgX0ObhIsdumFRLaL -DvDUmBPr2zuh/34j2XmWwn1yjN/WvGtdfhXW79Ki1S40HcWnygHgLV8sESFKUxxQ -mKvPUTwDOIwLFL5WtE8Mz7N++kgmDcmWMCHc8kcOIu73Ta/3D4imW7VbKgHZo9+K -3ESFE3RjAgMBAAECggEBAJTEIyjMqUT24G2FKiS1TiHvShBkTlQdoR5xvpZMlYbN -tVWxUmrAGqCQ/TIjYnfpnzCDMLhdwT48Ab6mQJw69MfiXwc1PvwX1e9hRscGul36 -ryGPKIVQEBsQG/zc4/L2tZe8ut+qeaK7XuYrPp8bk/X1e9qK5m7j+JpKosNSLgJj -NIbYsBkG2Mlq671irKYj2hVZeaBQmWmZxK4fw0Istz2WfN5nUKUeJhTwpR+JLUg4 -ELYYoB7EO0Cej9UBG30hbgu4RyXA+VbptJ+H042K5QJROUbtnLWuuWosZ5ATldwO -u03dIXL0SH0ao5NcWBzxU4F2sBXZRGP2x/jiSLHcqoECgYEA4qD7mXQpu1b8XO8U -6abpKloJCatSAHzjgdR2eRDRx5PMvloipfwqA77pnbjTUFajqWQgOXsDTCjcdQui -wf5XAaWu+TeAVTytLQbSiTsBhrnoqVrr3RoyDQmdnwHT8aCMouOgcC5thP9vQ8Us -rVdjvRRbnJpg3BeSNimH+u9AHgsCgYEA0EzcbOltCWPHRAY7B3Ge/AKBjBQr86Kv -TdpTlxePBDVIlH+BM6oct2gaSZZoHbqPjbq5v7yf0fKVcXE4bSVgqfDJ/sZQu9Lp -PTeV7wkk0OsAMKk7QukEpPno5q6tOTNnFecpUhVLLlqbfqkB2baYYwLJR3IRzboJ -FQbLY93E8gkCgYB+zlC5VlQbbNqcLXJoImqItgQkkuW5PCgYdwcrSov2ve5r/Acz -FNt1aRdSlx4176R3nXyibQA1Vw+ztiUFowiP9WLoM3PtPZwwe4bGHmwGNHPIfwVG -m+exf9XgKKespYbLhc45tuC08DATnXoYK7O1EnUINSFJRS8cezSI5eHcbQKBgQDC -PgqHXZ2aVftqCc1eAaxaIRQhRmY+CgUjumaczRFGwVFveP9I6Gdi+Kca3DE3F9Pq -PKgejo0SwP5vDT+rOGHN14bmGJUMsX9i4MTmZUZ5s8s3lXh3ysfT+GAhTd6nKrIE -kM3Nh6HWFhROptfc6BNusRh1kX/cspDplK5x8EpJ0QKBgQDWFg6S2je0KtbV5PYe -RultUEe2C0jYMDQx+JYxbPmtcopvZQrFEur3WKVuLy5UAy7EBvwMnZwIG7OOohJb -vkSpADK6VPn9lbqq7O8cTedEHttm6otmLt8ZyEl3hZMaL3hbuRj6ysjmoFKx6CrX -rK0/Ikt5ybqUzKCMJZg2VKGTxg== ------END PRIVATE KEY----- diff --git a/oath/core-test/src/test/secrets/rsa-public.pem b/oath/core-test/src/test/secrets/rsa-public.pem deleted file mode 100644 index e8d6288..0000000 --- a/oath/core-test/src/test/secrets/rsa-public.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4 -yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9 -83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs -WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT -69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8 -AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0 -YwIDAQAB ------END PUBLIC KEY----- diff --git a/oath/core/src/main/scala/io/oath/JwtClaims.scala b/oath/core/src/main/scala/io/oath/JwtClaims.scala index 7bcfaff..e555d28 100644 --- a/oath/core/src/main/scala/io/oath/JwtClaims.scala +++ b/oath/core/src/main/scala/io/oath/JwtClaims.scala @@ -1,6 +1,6 @@ package io.oath -sealed trait JwtClaims { +sealed abstract class JwtClaims { val registered: RegisteredClaims } diff --git a/oath/core/src/main/scala/io/oath/OathIssuer.scala b/oath/core/src/main/scala/io/oath/OathIssuer.scala index 435aa10..72f9041 100644 --- a/oath/core/src/main/scala/io/oath/OathIssuer.scala +++ b/oath/core/src/main/scala/io/oath/OathIssuer.scala @@ -2,6 +2,7 @@ package io.oath import io.oath.OathIssuer.JIssuer import io.oath.config.JwtIssuerConfig +import io.oath.macros.OathEnum import scala.util.chaining.scalaUtilChainingOps @@ -17,19 +18,13 @@ object OathIssuer { def as[S <: A](tokenType: S): JIssuer[S] = mapping(tokenType) } - def none[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathIssuer[A] = - getEnumValues[A].map { case (tokenType, _) => + def none[E: OathEnum]: OathIssuer[E] = + summon[OathEnum[E]].values.map { case (tokenType, _) => tokenType -> JwtIssuer(JwtIssuerConfig.none()) - }.toMap - .pipe(mapping => new JavaJwtOathIssuer(mapping)) + }.pipe(mapping => new JavaJwtOathIssuer(mapping)) - def createOrFail[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathIssuer[A] = - getEnumValues[A].map { case (tokenType, tokenConfig) => + def createOrFail[E: OathEnum]: OathIssuer[E] = + summon[OathEnum[E]].values.map { case (tokenType, tokenConfig) => tokenType -> JwtIssuerConfig.loadOrThrowOath(tokenConfig).pipe(JwtIssuer(_)) - }.toMap - .pipe(mapping => new JavaJwtOathIssuer(mapping)) + }.pipe(mapping => new JavaJwtOathIssuer(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/OathManager.scala b/oath/core/src/main/scala/io/oath/OathManager.scala index 0d91fba..76d7bee 100644 --- a/oath/core/src/main/scala/io/oath/OathManager.scala +++ b/oath/core/src/main/scala/io/oath/OathManager.scala @@ -2,6 +2,7 @@ package io.oath import io.oath.OathManager.JManager import io.oath.config.* +import io.oath.macros.OathEnum import scala.util.chaining.scalaUtilChainingOps @@ -17,19 +18,15 @@ object OathManager { def as[S <: A](tokenType: S): JManager[S] = mapping(tokenType) } - def none[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathManager[A] = - getEnumValues[A].map { case (tokenType, _) => + def none[E: OathEnum]: OathManager[E] = + summon[OathEnum[E]].values.map { case (tokenType, _) => tokenType -> JwtManager(JwtManagerConfig.none()) - }.toMap - .pipe(mapping => new JavaJwtOathManager(mapping)) + }.pipe(mapping => new JavaJwtOathManager(mapping)) - def createOrFail[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathManager[A] = - getEnumValues[A].map { case (tokenType, tokenConfig) => - tokenType -> JwtManagerConfig.loadOrThrowOath(tokenConfig).pipe(JwtManager(_)) - }.toMap - .pipe(mapping => new JavaJwtOathManager(mapping)) + def createOrFail[E: OathEnum]: OathManager[E] = + summon[OathEnum[E]].values.map { case (tokenType, tokenConfig) => + tokenType -> JwtManagerConfig + .loadOrThrowOath(tokenConfig) + .pipe(JwtManager(_)) + }.pipe(mapping => new JavaJwtOathManager(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/OathVerifier.scala b/oath/core/src/main/scala/io/oath/OathVerifier.scala index 4598234..71f4d97 100644 --- a/oath/core/src/main/scala/io/oath/OathVerifier.scala +++ b/oath/core/src/main/scala/io/oath/OathVerifier.scala @@ -2,6 +2,7 @@ package io.oath import io.oath.OathVerifier.JVerifier import io.oath.config.* +import io.oath.macros.OathEnum import scala.util.chaining.scalaUtilChainingOps @@ -18,19 +19,15 @@ object OathVerifier { def as[S <: A](tokenType: S): JVerifier[S] = mapping(tokenType) } - def none[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathVerifier[A] = - getEnumValues[A].map { case (tokenType, _) => + def none[E: OathEnum]: OathVerifier[E] = + summon[OathEnum[E]].values.map { case (tokenType, _) => tokenType -> JwtVerifier(JwtVerifierConfig.none()) - }.toMap - .pipe(mapping => new JavaJwtOathVerifier(mapping)) - - def createOrFail[A](using - m: scala.deriving.Mirror.SumOf[A] - ): OathVerifier[A] = - getEnumValues[A].map { case (tokenType, tokenConfig) => - tokenType -> JwtVerifierConfig.loadOrThrowOath(tokenConfig).pipe(JwtVerifier(_)) - }.toMap - .pipe(mapping => new JavaJwtOathVerifier(mapping)) + }.pipe(mapping => new JavaJwtOathVerifier(mapping)) + + def createOrFail[E: OathEnum]: OathVerifier[E] = + summon[OathEnum[E]].values.map { case (tokenType, tokenConfig) => + tokenType -> JwtVerifierConfig + .loadOrThrowOath(tokenConfig) + .pipe(JwtVerifier(_)) + }.pipe(mapping => new JavaJwtOathVerifier(mapping)) } diff --git a/oath/core/src/main/scala/io/oath/package.scala b/oath/core/src/main/scala/io/oath/package.scala index 51073ca..417650b 100644 --- a/oath/core/src/main/scala/io/oath/package.scala +++ b/oath/core/src/main/scala/io/oath/package.scala @@ -1,20 +1,10 @@ package io.oath import com.auth0.jwt.interfaces.DecodedJWT -import io.oath.macros.OathEnumMacro -import io.oath.utils.Formatter import java.time.Instant import scala.jdk.CollectionConverters.CollectionHasAsScala -// TODO: Move to a file and test it properly -inline def getEnumValues[A](using - m: scala.deriving.Mirror.SumOf[A] -): Set[(A, String)] = - OathEnumMacro - .enumValues[A] - .map(value => value -> Formatter.convertUpperCamelToLowerHyphen(value.toString)) - // TODO: Move to file extension (decodedJWT: DecodedJWT) { def getOptionIssuer: Option[String] = Option(decodedJWT.getIssuer) diff --git a/oath/core/src/main/scala/io/oath/utils/Base64.scala b/oath/core/src/main/scala/io/oath/utils/Base64.scala index 0733c12..ac3d151 100644 --- a/oath/core/src/main/scala/io/oath/utils/Base64.scala +++ b/oath/core/src/main/scala/io/oath/utils/Base64.scala @@ -8,7 +8,6 @@ import scala.util.control.Exception.allCatch private[oath] object Base64 { - // TODO: not tested def decodeToken(token: String): Either[JwtVerifyError.DecodingError, String] = allCatch .withTry(new String(JBase64.getUrlDecoder.decode(token), StandardCharsets.UTF_8)) diff --git a/oath/core/src/main/scala/io/oath/utils/Formatter.scala b/oath/core/src/main/scala/io/oath/utils/Formatter.scala deleted file mode 100644 index f45609d..0000000 --- a/oath/core/src/main/scala/io/oath/utils/Formatter.scala +++ /dev/null @@ -1,8 +0,0 @@ -package io.oath.utils - -private[oath] object Formatter { - - // TODO: not tested properly - def convertUpperCamelToLowerHyphen(str: String): String = - str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim -} diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala index 7b699f3..4fadb1d 100644 --- a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -12,7 +12,7 @@ import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.Try import scala.util.chaining.* -class JwtIssuerSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { +class JwtIssuerSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { val jwtVerifier = JWT .require(Algorithm.none()) diff --git a/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala index 54ff34c..8db6ed0 100644 --- a/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala @@ -4,7 +4,7 @@ import io.oath.config.* import io.oath.syntax.all.* import io.oath.testkit.* -class JwtManagerSpec extends AnyWordSpecBase, PropertyBasedTesting { +class JwtManagerSpec extends WordSpecBase, PropertyBasedTesting { "JwtManager" should { "be able to issue and verify jwt tokens without claims" in forAll { (config: JwtManagerConfig) => diff --git a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala index e169b6e..92a271e 100644 --- a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -12,7 +12,7 @@ import io.oath.testkit.* import scala.util.chaining.scalaUtilChainingOps -class JwtVerifierSpec extends AnyWordSpecBase, PropertyBasedTesting, ClockHelper { +class JwtVerifierSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { val defaultConfig = JwtVerifierConfig( diff --git a/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala b/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala index bccdadc..8d6c616 100644 --- a/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/OathIssuerSpec.scala @@ -1,9 +1,9 @@ package io.oath import io.oath.OathIssuer.JIssuer -import io.oath.testkit.AnyWordSpecBase +import io.oath.testkit.WordSpecBase -class OathIssuerSpec extends AnyWordSpecBase { +class OathIssuerSpec extends WordSpecBase { "OathIssuer" should { "create jwt token issuers" in { diff --git a/oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala b/oath/core/src/test/scala/io/oath/OathManagerSpec.scala similarity index 92% rename from oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala rename to oath/core/src/test/scala/io/oath/OathManagerSpec.scala index 886cc95..f7c464e 100644 --- a/oath/core-test/src/test/scala/io/oath/OathManagerSpec.scala +++ b/oath/core/src/test/scala/io/oath/OathManagerSpec.scala @@ -1,9 +1,10 @@ package io.oath +import io.oath.OathManager.JManager import io.oath.syntax.all.* -import io.oath.test.* +import io.oath.testkit.WordSpecBase -class OathManagerSpec extends AnyWordSpecBase { +class OathManagerSpec extends WordSpecBase { "OathManager" should { "create different token managers" in { diff --git a/oath/core/src/test/scala/io/oath/OathToken.scala b/oath/core/src/test/scala/io/oath/OathToken.scala index d15971b..e35c8b6 100644 --- a/oath/core/src/test/scala/io/oath/OathToken.scala +++ b/oath/core/src/test/scala/io/oath/OathToken.scala @@ -1,5 +1,7 @@ package io.oath -enum OathToken { +import io.oath.macros.OathEnum + +enum OathToken derives OathEnum { case AccessToken, RefreshToken, ActivationEmailToken, ForgotPasswordToken } diff --git a/oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala b/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala similarity index 92% rename from oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala rename to oath/core/src/test/scala/io/oath/OathVerifierSpec.scala index 49392e7..6ece294 100644 --- a/oath/core-test/src/test/scala/io/oath/OathVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala @@ -1,9 +1,11 @@ package io.oath +import io.oath.OathIssuer.JIssuer +import io.oath.OathVerifier.JVerifier import io.oath.syntax.all.* -import io.oath.test.* +import io.oath.testkit.WordSpecBase -class OathVerifierSpec extends AnyWordSpecBase { +class OathVerifierSpec extends WordSpecBase { val oathIssuer = OathIssuer.createOrFail[OathToken] diff --git a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala index 12c1b0e..3aa3237 100644 --- a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -4,7 +4,7 @@ import com.auth0.jwt.JWT import com.typesafe.config.ConfigFactory import io.oath.testkit.* -class AlgorithmLoaderSpec extends AnyWordSpecBase, PropertyBasedTesting { +class AlgorithmLoaderSpec extends WordSpecBase, PropertyBasedTesting { val AlgorithmConfigLocation = "algorithm" "AlgorithmLoader" should { diff --git a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index 8500626..6ab6ce4 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -5,7 +5,7 @@ import io.oath.testkit.* import scala.concurrent.duration.DurationInt -class JwtIssuerLoaderSpec extends AnyWordSpecBase { +class JwtIssuerLoaderSpec extends WordSpecBase { val configFile = "issuer" val DefaultTokenConfigLocation = "default-token" diff --git a/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala index 917f95e..d7dcbc0 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala @@ -5,7 +5,7 @@ import io.oath.testkit.* import scala.concurrent.duration.DurationInt -class JwtManagerLoaderSpec extends AnyWordSpecBase { +class JwtManagerLoaderSpec extends WordSpecBase { val configFile = "manager" val TokenConfigLocation = "token" diff --git a/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala index 0e77572..794b3ce 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -5,7 +5,7 @@ import io.oath.testkit.* import scala.concurrent.duration.DurationInt -class JwtVerifierLoaderSpec extends AnyWordSpecBase { +class JwtVerifierLoaderSpec extends WordSpecBase { val configFile = "verifier" val DefaultTokenConfigLocation = "default-token" diff --git a/oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala b/oath/core/src/test/scala/io/oath/testkit/WordSpecBase.scala similarity index 61% rename from oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala rename to oath/core/src/test/scala/io/oath/testkit/WordSpecBase.scala index 0fdce24..91dee10 100644 --- a/oath/core/src/test/scala/io/oath/testkit/AnyWordSpecBase.scala +++ b/oath/core/src/test/scala/io/oath/testkit/WordSpecBase.scala @@ -4,4 +4,4 @@ import org.scalatest.matchers.should import org.scalatest.wordspec.AnyWordSpec import org.scalatest.{EitherValues, OptionValues} -abstract class AnyWordSpecBase extends AnyWordSpec, should.Matchers, OptionValues, EitherValues +abstract class WordSpecBase extends AnyWordSpec, should.Matchers, OptionValues, EitherValues diff --git a/oath/core/src/test/scala/io/oath/utils/Base64Spec.scala b/oath/core/src/test/scala/io/oath/utils/Base64Spec.scala new file mode 100644 index 0000000..a9f0fe9 --- /dev/null +++ b/oath/core/src/test/scala/io/oath/utils/Base64Spec.scala @@ -0,0 +1,14 @@ +package io.oath.utils + +import io.oath.testkit.WordSpecBase + +class Base64Spec extends WordSpecBase { + + "Base64" when { + ".decodeToken" should {} + "decode token" in { + val str = "dGVzdA==" + Base64.decodeToken(str) shouldBe Right("test") + } + } +} diff --git a/oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala b/oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala deleted file mode 100644 index 2f53832..0000000 --- a/oath/core/src/test/scala/io/oath/utils/FormatterSpec.scala +++ /dev/null @@ -1,29 +0,0 @@ -package io.oath.utils - -import io.oath.testkit.* - -class FormatterSpec extends AnyWordSpecBase { - - "Formatter" should { - "convert upper camel case to lower hyphen" in { - val res1 = Formatter.convertUpperCamelToLowerHyphen("HelloWorld") - val res2 = Formatter.convertUpperCamelToLowerHyphen(" Hello World ") - - val expected = "hello-world" - - res1 shouldBe expected - res2 shouldBe expected - } - - "convert scala enum string values to lower hyphen" in { - enum SomeEnum: - case firstEnum, SecondEnum, Third, ForthEnumValue - - val expected = Seq("first-enum", "second-enum", "third", "forth-enum-value") - - SomeEnum.values.toSeq - .map(_.toString) - .map(Formatter.convertUpperCamelToLowerHyphen) should contain theSameElementsAs expected - } - } -} diff --git a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala index 38e3a13..58c99b7 100644 --- a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala +++ b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala @@ -8,27 +8,26 @@ import io.oath.config.JwtVerifierConfig.* import io.oath.config.{JwtIssuerConfig, JwtVerifierConfig} import io.oath.json.ClaimsDecoder import io.oath.syntax.* -import io.oath.testkit.AnyWordSpecBase -import io.oath.utils.CodecUtils +import io.oath.syntax.all.* +import io.oath.testkit.CodecHelper.unsafeParseJsonToJavaMap +import io.oath.testkit.WordSpecBase -class JsoniterConversionSpec extends AnyWordSpecBase, CodecUtils { +class JsoniterConversionSpec extends WordSpecBase { val verifierConfig = JwtVerifierConfig( Algorithm.none(), - None, ProvidedWithConfig(None, None, Nil), LeewayWindowConfig(None, None, None, None), ) val issuerConfig = JwtIssuerConfig( Algorithm.none(), - None, RegisteredConfig(None, None, Nil, includeJwtIdClaim = false, includeIssueAtClaim = false, None, None), ) - val jwtVerifier = new JwtVerifierSpec(verifierConfig) - val jwtIssuer = new JwtIssuer(issuerConfig) + val jwtVerifier = JwtVerifier(verifierConfig) + val jwtIssuer = JwtIssuer(issuerConfig) "JsoniterConversion" should { diff --git a/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala b/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala deleted file mode 100644 index 6ca4ead..0000000 --- a/oath/macros/src/main/scala/io/oath/macros/EnumExtensions.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.oath.macros - -trait EnumExtension[E] { - def values: Set[E] -} - -object EnumExtension { - - inline def apply[E](using ) -} \ No newline at end of file diff --git a/oath/macros/src/main/scala/io/oath/macros/Main.scala b/oath/macros/src/main/scala/io/oath/macros/Main.scala deleted file mode 100644 index 4340aaf..0000000 --- a/oath/macros/src/main/scala/io/oath/macros/Main.scala +++ /dev/null @@ -1,10 +0,0 @@ -package io.oath.macros - -object Main extends App { - - enum Color { - case Red, Green, Blue - } - - val values = -} diff --git a/oath/macros/src/main/scala/io/oath/macros/OathEnum.scala b/oath/macros/src/main/scala/io/oath/macros/OathEnum.scala new file mode 100644 index 0000000..d2b9c54 --- /dev/null +++ b/oath/macros/src/main/scala/io/oath/macros/OathEnum.scala @@ -0,0 +1,32 @@ +package io.oath.macros + +import scala.annotation.nowarn + +trait OathEnum[E] { + def values: Map[E, String] +} + +object OathEnum { + + inline private def summonSumInstances[ET <: Tuple, E](acc: Set[E]): Set[E] = { + import scala.compiletime.* + + inline erasedValue[ET] match { + case _: EmptyTuple => acc + case _: (t *: ts) => + val enumValue = summonInline[ValueOf[t]].value.asInstanceOf[E] + summonSumInstances[ts, E](acc + enumValue) + } + } + + private def convertUpperCamelToLowerHyphen(str: String): String = + str.split("(?=\\p{Lu})").map(_.trim.toLowerCase).filter(_.nonEmpty).mkString("-").trim + + @nowarn("msg=New anonymous class definition will be duplicated at each inline site") + inline def derived[E](using m: scala.deriving.Mirror.SumOf[E]): OathEnum[E] = + new OathEnum[E] { + def values: Map[E, String] = summonSumInstances[m.MirroredElemTypes, E](Set.empty) + .map(e => e -> convertUpperCamelToLowerHyphen(e.toString)) + .toMap + } +} diff --git a/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala b/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala deleted file mode 100644 index d3dd2c6..0000000 --- a/oath/macros/src/main/scala/io/oath/macros/OathEnumMacro.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.oath.macros - -object OathEnumMacro { - - inline def enumValues[T](using - m: scala.deriving.Mirror.SumOf[T] - ): Set[T] = - allInstances[m.MirroredElemTypes, m.MirroredType].toSet - - inline def allInstances[ET <: Tuple, T]: List[T] = - import scala.compiletime.* - - inline erasedValue[ET] match - case _: EmptyTuple => Nil - case _: (t *: ts) => - summonInline[ValueOf[t]].value.asInstanceOf[T] :: allInstances[ts, T] -} diff --git a/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala deleted file mode 100644 index 288930a..0000000 --- a/oath/macros/src/test/scala/io/oath/macros/OathEnumMacroSpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -package io.oath.macros - -import org.scalatest.matchers.should -import org.scalatest.wordspec.AnyWordSpec - -class OathEnumMacroSpec extends AnyWordSpec with should.Matchers { - - enum Foo { - case Foo1, Foo2, Foo3, Foo4 - } - - "OathEnumMacros" should { - "discover all children directories (enum values) in a Sum type" in { - val fooChildren = OathEnumMacro.enumValues[Foo] - fooChildren should contain theSameElementsAs Set(Foo.Foo1, Foo.Foo2, Foo.Foo3, Foo.Foo4) - } - } -} diff --git a/oath/macros/src/test/scala/io/oath/macros/OathEnumSpec.scala b/oath/macros/src/test/scala/io/oath/macros/OathEnumSpec.scala new file mode 100644 index 0000000..c24eb43 --- /dev/null +++ b/oath/macros/src/test/scala/io/oath/macros/OathEnumSpec.scala @@ -0,0 +1,91 @@ +package io.oath.macros + +import org.scalatest.matchers.should +import org.scalatest.wordspec.AnyWordSpec + +class OathEnumSpec extends AnyWordSpec with should.Matchers { + + enum Foo derives OathEnum { + case Foo1, FooBar, fooBar + } + + sealed trait Bar derives OathEnum + + object Bar { + case object Bar1 extends Bar + case object Bar2 extends Bar + case object Bar3 extends Bar + case object Bar4 extends Bar + } + + sealed abstract class FooBar derives OathEnum + + object FooBar { + case object FooBar1 extends FooBar + case object FooBar2 extends FooBar + case object fooBar3 extends FooBar + } + +// This should fail to compile because sealed class is not supported +// sealed class FooBar derives OathEnum +// +// object FooBar { +// case object Bar1 extends FooBar +// case object Bar4 extends FooBar +// } + +// This should fail to compile because enum contains not product type +// enum FooIllegal derives OathEnum { +// case FooIllegal1(value: String) extends FooIllegal +// case FooIllegal2 extends FooIllegal +// } + +// This should fail to compile because sealed trait contains not product type +// sealed trait BarIllegal derives OathEnum +// object BarIllegal { +// case class BarIllegal1(value: String) extends BarIllegal +// case object BarIllegal2 extends BarIllegal +// case object BarIllegal3 extends BarIllegal +// } + + "OathEnum" when { + ".value" should { + "discover all children directories for Enum" in { + val oathEnum = summon[OathEnum[Foo]] + oathEnum.values should contain theSameElementsAs + Map(Foo.Foo1 -> "foo1", Foo.FooBar -> "foo-bar", Foo.fooBar -> "foo-bar") + } + + "discover all children directories for sealed trait" in { + val children = summon[OathEnum[Bar]] + children.values should contain theSameElementsAs Map( + Bar.Bar1 -> "bar1", + Bar.Bar2 -> "bar2", + Bar.Bar3 -> "bar3", + Bar.Bar4 -> "bar4", + ) + } + + "discover all children directories for sealed abstract class" in { + val children = summon[OathEnum[FooBar]] + children.values should contain theSameElementsAs Map( + FooBar.FooBar1 -> "foo-bar1", + FooBar.FooBar2 -> "foo-bar2", + FooBar.fooBar3 -> "foo-bar3", + ) + } + +// "fail to compile children directories when sealed trait enum contains not enum type" in { +// val children = EnumValues[FooIllegal] +// } +// +// "fail to compile children directories when sealed trait contains not enum type" in { +// val children = EnumValues[BarIllegal] +// } +// +// "fail to compile for sealed abstract class" in { +// val children = EnumValues[FooBarIllegal] +// } + } + } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 15d966c..b113890 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,8 +8,8 @@ object Dependencies { private lazy val javaJWTV = "4.4.0" private lazy val configV = "1.4.3" private lazy val bcprovV = "1.78.1" - private lazy val circeV = "0.14.7" - private lazy val jsoniterScalaV = "2.27.3" + private lazy val circeV = "0.14.9" + private lazy val jsoniterScalaV = "2.30.7" private lazy val catsV = "2.12.0" private lazy val tinkV = "1.14.1" diff --git a/project/plugins.sbt b/project/plugins.sbt index 55f1dc6..c3b53b9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0") addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.6.7") From 5426fb2d5ae9baa440eb3a330b1b6fba97e3de3f Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 8 Dec 2024 01:30:15 +0000 Subject: [PATCH 11/15] feat: generate workflow --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 919c32a..e74c26f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,11 +95,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) - run: mkdir -p oath/circe/target oath/target oath/jsoniter-scala/target example/target oath/core/target oath/macros/target project/target + run: mkdir -p oath/circe/target oath/jsoniter-scala/target oath/core/target oath/macros/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) - run: tar cf targets.tar oath/circe/target oath/target oath/jsoniter-scala/target example/target oath/core/target oath/macros/target project/target + run: tar cf targets.tar oath/circe/target oath/jsoniter-scala/target oath/core/target oath/macros/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) @@ -252,7 +252,7 @@ jobs: - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: - modules-ignore: oath_3 + modules-ignore: oath-root_3 example_3 oath_3 configs-ignore: test scala-tool scala-doc-tool test-internal validate-steward: @@ -285,7 +285,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [3.3.3] + scala: [3.3.4] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -313,7 +313,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [3.3.3] + scala: [3.3.4] java: [temurin@11] runs-on: ${{ matrix.os }} steps: From 822c2f95a20c0bbb7b9975f02a6b84e8204ec061 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Thu, 26 Dec 2024 23:14:07 +0000 Subject: [PATCH 12/15] feat: progress --- build.sbt | 2 +- .../src/main/scala/io/oath/JwtIssueError.scala | 7 +++---- .../core/src/main/scala/io/oath/JwtIssuer.scala | 2 +- .../scala/io/oath/config/AlgorithmLoader.scala | 10 ++++++---- .../scala/io/oath/syntax/JwtBuilderOps.scala | 2 +- .../src/test/scala/io/oath/JwtIssuerSpec.scala | 4 ++-- project/Dependencies.scala | 17 +++++++++++------ 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index 81758c2..16766cd 100644 --- a/build.sbt +++ b/build.sbt @@ -96,7 +96,7 @@ lazy val oathCore = createOathModule(Some("core")) Dependencies.javaJWT, Dependencies.typesafeConfig, Dependencies.bcprov, - Dependencies.cats, + Dependencies.catsCore, Dependencies.tink, Dependencies.scalaTest % Test, Dependencies.scalaTestPlusScalaCheck % Test, diff --git a/oath/core/src/main/scala/io/oath/JwtIssueError.scala b/oath/core/src/main/scala/io/oath/JwtIssueError.scala index 9960a83..a7ef0bc 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssueError.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssueError.scala @@ -2,11 +2,10 @@ package io.oath import cats.syntax.all.* -sealed abstract class JwtIssueError(error: String, cause: Option[Throwable] = None) - extends Exception(error, cause.orNull) +sealed abstract class JwtIssueError(error: String, cause: Throwable) extends Exception(error, cause) object JwtIssueError { - final case class SignError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) + final case class SignError(message: String)(underlying: Throwable) extends JwtIssueError(message, underlying) - final case class EncodeError(message: String, underlying: Throwable) extends JwtIssueError(message, underlying.some) + final case class EncodeError(message: String)(underlying: Throwable) extends JwtIssueError(message, underlying) } diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index a89cd8d..307d2f3 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -67,7 +67,7 @@ object JwtIssuer { .withTry(builder.sign(algorithm)) .toEither .left - .map(e => JwtIssueError.SignError("Signing token failed", e)) + .map(JwtIssueError.SignError("Signing token failed")) def issueJwt( claims: JwtClaims.Claims = JwtClaims.Claims() diff --git a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala index 988ab93..04eb688 100644 --- a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala +++ b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala @@ -28,31 +28,33 @@ object AlgorithmLoader { algorithmScoped: Config, forIssuing: Boolean, ): (Option[RSAPrivateKey], Option[RSAPublicKey]) = - if forIssuing then + if (forIssuing) { val privateKey: RSAPrivateKey = loadPrivateKey(algorithmScoped, RSAKeyFactory) .map(_.asInstanceOf[RSAPrivateKey]) .fold(error => throw new IllegalArgumentException(s"Fail to load RSA Private key pem file: $error"), identity) (Some(privateKey), None) - else + } else { val publicKey: RSAPublicKey = loadPublicKey(algorithmScoped, RSAKeyFactory) .map(_.asInstanceOf[RSAPublicKey]) .fold(error => throw new IllegalArgumentException(s"Fail to load RSA Public key pem file: $error"), identity) (None, Some(publicKey)) + } private def loadECKeyOrThrow( algorithmScoped: Config, forIssuing: Boolean, ): (Option[ECPrivateKey], Option[ECPublicKey]) = - if forIssuing then + if (forIssuing) { val privateKey: ECPrivateKey = loadPrivateKey(algorithmScoped, ECKeyFactory) .map(_.asInstanceOf[ECPrivateKey]) .fold(error => throw new IllegalArgumentException(s"Failed to load EC Private key pem file: $error"), identity) (Some(privateKey), None) - else + } else { val publicKey: ECPublicKey = loadPublicKey(algorithmScoped, ECKeyFactory) .map(_.asInstanceOf[ECPublicKey]) .fold(error => throw new IllegalArgumentException(s"Failed to load EC Public key pem file: $error"), identity) (None, Some(publicKey)) + } private def loadPublicKey(algorithmScoped: Config, keyFactory: KeyFactory): Either[String, PublicKey] = algorithmScoped diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala index 0653c2d..e965109 100644 --- a/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala +++ b/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala @@ -21,7 +21,7 @@ private[oath] trait JwtBuilderOps { ) .toEither .left - .map(error => JwtIssueError.EncodeError("Failed when trying to encode token", error)) + .map(JwtIssueError.EncodeError("Failed when trying to encode token")) def safeEncodeHeader[H](claims: H)(using ClaimsEncoder[H] diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala index 4fadb1d..5b81b38 100644 --- a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -181,8 +181,8 @@ class JwtIssuerSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { .asInstanceOf[JwtIssueError.SignError] signError.message shouldBe "Signing token failed" - signError.underlying shouldBe a[IllegalArgumentException] - signError.underlying.getMessage shouldBe "The Algorithm cannot be null." + signError.getCause shouldBe a[IllegalArgumentException] + signError.getCause.getMessage shouldBe "The Algorithm cannot be null." } } } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b113890..c985873 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,28 +4,33 @@ object Dependencies { private lazy val scalaTestV = "3.2.19" private lazy val scalaTestPlusCheckV = "3.2.18.0" - private lazy val scalacheckV = "1.17.1" + private lazy val scalacheckV = "1.18.1" private lazy val javaJWTV = "4.4.0" private lazy val configV = "1.4.3" - private lazy val bcprovV = "1.78.1" - private lazy val circeV = "0.14.9" - private lazy val jsoniterScalaV = "2.30.7" + private lazy val bcprovV = "1.79" + private lazy val circeV = "0.14.10" + private lazy val jsoniterScalaV = "2.31.3" private lazy val catsV = "2.12.0" - private lazy val tinkV = "1.14.1" + private lazy val tinkV = "1.15.0" + // Testing lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestV lazy val scalaTestPlusScalaCheck = "org.scalatestplus" %% "scalacheck-1-17" % scalaTestPlusCheckV lazy val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheckV + // Circe lazy val circeCore = "io.circe" %% "circe-core" % circeV lazy val circeGeneric = "io.circe" %% "circe-generic" % circeV lazy val circeParser = "io.circe" %% "circe-parser" % circeV + // Jsoniter-scala lazy val jsoniterScalacore = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % jsoniterScalaV lazy val jsoniterScalamacros = "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterScalaV % "provided" - lazy val cats = "org.typelevel" %% "cats-core" % catsV + // Typelevel + lazy val catsCore = "org.typelevel" %% "cats-core" % catsV + lazy val typesafeConfig = "com.typesafe" % "config" % configV lazy val bcprov = "org.bouncycastle" % "bcprov-jdk18on" % bcprovV lazy val tink = "com.google.crypto.tink" % "tink" % tinkV From 5e2d0f123adeafae22f076e94d9c334b9a9fac2b Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sat, 8 Feb 2025 14:00:46 +0000 Subject: [PATCH 13/15] feat: move to scala3 --- .../src/main/scala/io/oath/JwtClaims.scala | 20 +- .../main/scala/io/oath/JwtIssueError.scala | 2 - .../src/main/scala/io/oath/JwtIssuer.scala | 41 +- .../scala/io/oath/syntax/JwtBuilderOps.scala | 36 -- .../main/scala/io/oath/syntax/internal.scala | 3 - .../test/scala/io/oath/JwtIssuerSpec.scala | 302 ++++++------ .../test/scala/io/oath/JwtVerifierSpec.scala | 446 +++++++++--------- 7 files changed, 434 insertions(+), 416 deletions(-) delete mode 100644 oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala delete mode 100644 oath/core/src/main/scala/io/oath/syntax/internal.scala diff --git a/oath/core/src/main/scala/io/oath/JwtClaims.scala b/oath/core/src/main/scala/io/oath/JwtClaims.scala index e555d28..6c8c2f3 100644 --- a/oath/core/src/main/scala/io/oath/JwtClaims.scala +++ b/oath/core/src/main/scala/io/oath/JwtClaims.scala @@ -1,16 +1,20 @@ package io.oath -sealed abstract class JwtClaims { - val registered: RegisteredClaims -} +sealed abstract class JwtClaims(val registered: RegisteredClaims) object JwtClaims { - final case class Claims(registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims + final case class Claims(override val registered: RegisteredClaims = RegisteredClaims.empty) + extends JwtClaims(registered) - final case class ClaimsH[+H](header: H, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims + final case class ClaimsH[+H](header: H, override val registered: RegisteredClaims = RegisteredClaims.empty) + extends JwtClaims(registered) - final case class ClaimsP[+P](payload: P, registered: RegisteredClaims = RegisteredClaims.empty) extends JwtClaims + final case class ClaimsP[+P](payload: P, override val registered: RegisteredClaims = RegisteredClaims.empty) + extends JwtClaims(registered) - final case class ClaimsHP[+H, +P](header: H, payload: P, registered: RegisteredClaims = RegisteredClaims.empty) - extends JwtClaims + final case class ClaimsHP[+H, +P]( + header: H, + payload: P, + override val registered: RegisteredClaims = RegisteredClaims.empty, + ) extends JwtClaims(registered) } diff --git a/oath/core/src/main/scala/io/oath/JwtIssueError.scala b/oath/core/src/main/scala/io/oath/JwtIssueError.scala index a7ef0bc..9f0b3a4 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssueError.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssueError.scala @@ -1,7 +1,5 @@ package io.oath -import cats.syntax.all.* - sealed abstract class JwtIssueError(error: String, cause: Throwable) extends Exception(error, cause) object JwtIssueError { diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index 307d2f3..d6833c2 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -4,7 +4,6 @@ import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.{JWT, JWTCreator} import io.oath.config.* import io.oath.json.ClaimsEncoder -import io.oath.syntax.internal.* import java.time.temporal.ChronoUnit import java.time.{Clock, Instant} @@ -62,6 +61,34 @@ object JwtIssuer { ) } + private def safeEncodeHeader[T]( + jwtBuilder: JWTCreator.Builder, + claims: T, + )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, JWTCreator.Builder] = + allCatch + .withTry( + claimsEncoder + .encode(claims) + .pipe(jwtBuilder.withHeader) + ) + .toEither + .left + .map(JwtIssueError.EncodeError("Failed when trying to encode header")) + + private def safeEncodePayload[T]( + jwtBuilder: JWTCreator.Builder, + claims: T, + )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, JWTCreator.Builder] = + allCatch + .withTry( + claimsEncoder + .encode(claims) + .pipe(jwtBuilder.withPayload) + ) + .toEither + .left + .map(JwtIssueError.EncodeError("Failed when trying to encode payload")) + private def safeSign(builder: JWTCreator.Builder, algorithm: Algorithm): Either[JwtIssueError, String] = allCatch .withTry(builder.sign(algorithm)) @@ -91,7 +118,7 @@ object JwtIssuer { ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] = { val jwtBuilder = JWT.create() for - headerBuilder <- jwtBuilder.safeEncodeHeader(claims.header) + headerBuilder <- safeEncodeHeader(jwtBuilder, claims.header) registeredClaims = setRegisteredClaims(claims.registered) builder = buildJwt(headerBuilder, registeredClaims) token <- safeSign(builder, config.algorithm) @@ -107,7 +134,7 @@ object JwtIssuer { ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsP[P]]] = { val jwtBuilder = JWT.create() for - payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) + payloadBuilder <- safeEncodePayload(jwtBuilder, claims.payload) registeredClaims = setRegisteredClaims(claims.registered) builder = buildJwt(payloadBuilder, registeredClaims) token <- safeSign(builder, config.algorithm) @@ -122,9 +149,9 @@ object JwtIssuer { claims: JwtClaims.ClaimsHP[H, P] )(using ClaimsEncoder[H], ClaimsEncoder[P]): Either[JwtIssueError, Jwt[JwtClaims.ClaimsHP[H, P]]] = { val jwtBuilder = JWT.create() - for - payloadBuilder <- jwtBuilder.safeEncodePayload(claims.payload) - headerAndPayloadBuilder <- payloadBuilder.safeEncodeHeader(claims.header) + for { + payloadBuilder <- safeEncodePayload(jwtBuilder, claims.payload) + headerAndPayloadBuilder <- safeEncodeHeader(payloadBuilder, claims.header) registeredClaims = setRegisteredClaims(claims.registered) builder = buildJwt(headerAndPayloadBuilder, registeredClaims) token <- safeSign(builder, config.algorithm) @@ -132,7 +159,7 @@ object JwtIssuer { claims.copy(registered = registeredClaims), token, ) - yield jwt + } yield jwt } } diff --git a/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala b/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala deleted file mode 100644 index e965109..0000000 --- a/oath/core/src/main/scala/io/oath/syntax/JwtBuilderOps.scala +++ /dev/null @@ -1,36 +0,0 @@ -package io.oath.syntax - -import com.auth0.jwt.JWTCreator.Builder -import io.oath.JwtIssueError -import io.oath.json.ClaimsEncoder - -import scala.util.chaining.scalaUtilChainingOps -import scala.util.control.Exception.allCatch - -private[oath] trait JwtBuilderOps { - extension (builder: Builder) { - private def safeEncode[T]( - claims: T, - toBuilder: String => Builder, - )(using claimsEncoder: ClaimsEncoder[T]): Either[JwtIssueError.EncodeError, Builder] = - allCatch - .withTry( - claimsEncoder - .encode(claims) - .pipe(toBuilder) - ) - .toEither - .left - .map(JwtIssueError.EncodeError("Failed when trying to encode token")) - - def safeEncodeHeader[H](claims: H)(using - ClaimsEncoder[H] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withHeader) - - def safeEncodePayload[P](claims: P)(using - ClaimsEncoder[P] - ): Either[JwtIssueError.EncodeError, Builder] = - safeEncode(claims, builder.withPayload) - } -} diff --git a/oath/core/src/main/scala/io/oath/syntax/internal.scala b/oath/core/src/main/scala/io/oath/syntax/internal.scala deleted file mode 100644 index 08864fb..0000000 --- a/oath/core/src/main/scala/io/oath/syntax/internal.scala +++ /dev/null @@ -1,3 +0,0 @@ -package io.oath.syntax - -private[oath] object internal extends JwtBuilderOps diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala index 5b81b38..b604d08 100644 --- a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -19,170 +19,184 @@ class JwtIssuerSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { .acceptLeeway(5) .build() - "JwtIssuer" should { - "issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val jwtIssuer = JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt().value - - val decodedJWT = jwtVerifier.verify(jwtClaims.token) - - Option(decodedJWT.getIssuer) shouldBe config.registered.issuerClaim - Option(decodedJWT.getSubject) shouldBe config.registered.subjectClaim - Option(decodedJWT.getAudience) - .map(_.asScala.toSeq) - .toSeq - .flatten shouldBe config.registered.audienceClaims - - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe Option.when(config.registered.includeIssueAtClaim)(now) - - if (config.registered.includeJwtIdClaim) - Option(decodedJWT.getId) should not be empty - else - Option(decodedJWT.getId) shouldBe empty - - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe config.registered.expiresAtOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe config.registered.notBeforeOffset.map(offset => - now.plusSeconds(offset.toSeconds) - ) - } - - "issue token with predefine configure claims and ad-hoc registered claims" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + "JwtIssuer" when { + "issueJwt Claims" should { + "successfully issue token with predefine configure claims" in forAll { (config: JwtIssuerConfig) => val now = getInstantNowSeconds val jwtIssuer = JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(registeredClaims.toClaims).value - - val expectedIssuer = registeredClaims.iss orElse config.registered.issuerClaim - val expectedSubject = registeredClaims.sub orElse config.registered.subjectClaim - val expectedAudience = - if (registeredClaims.aud.nonEmpty) registeredClaims.aud else config.registered.audienceClaims - val expectedIssuedAt = registeredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now) - val expectedExpiredAt = - registeredClaims.exp orElse config.registered.expiresAtOffset.map(offset => now.plusSeconds(offset.toSeconds)) - val expectedNotBefore = - registeredClaims.nbf orElse config.registered.notBeforeOffset.map(offset => now.plusSeconds(offset.toSeconds)) - - jwtClaims.claims.registered.iss shouldBe expectedIssuer - jwtClaims.claims.registered.sub shouldBe expectedSubject - jwtClaims.claims.registered.aud shouldBe expectedAudience - jwtClaims.claims.registered.iat shouldBe expectedIssuedAt - jwtClaims.claims.registered.exp shouldBe expectedExpiredAt - jwtClaims.claims.registered.nbf shouldBe expectedNotBefore - - if (registeredClaims.jti.nonEmpty) - jwtClaims.claims.registered.jti shouldBe registeredClaims.jti - else if (config.registered.includeJwtIdClaim) - jwtClaims.claims.registered.jti should not be empty - else jwtClaims.claims.registered.jti shouldBe empty - } - - "issue token with only registered claims empty strings" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val adHocRegisteredClaims = - registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value - - val decodedJWT = jwtVerifier.verify(jwtClaims.token) - - Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss - Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub - Option(decodedJWT.getAudience) - .map(_.asScala.toSeq) - .toSeq - .flatten shouldBe jwtClaims.claims.registered.aud - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat - Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf - } - - "issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll { - (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => - val now = getInstantNowSeconds - val adHocRegisteredClaims = - registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) - val jwtIssuer = JwtIssuer(config, getFixedClock(now)) - val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value + val jwtClaims = jwtIssuer.issueJwt().value val decodedJWT = jwtVerifier.verify(jwtClaims.token) - Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss - Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub + Option(decodedJWT.getIssuer) shouldBe config.registered.issuerClaim + Option(decodedJWT.getSubject) shouldBe config.registered.subjectClaim Option(decodedJWT.getAudience) .map(_.asScala.toSeq) .toSeq - .flatten shouldBe jwtClaims.claims.registered.aud - Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat - Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti - Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp - Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf - } - - "issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => - val jwtIssuer = JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(header.toClaimsH).value + .flatten shouldBe config.registered.audienceClaims + + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe Option.when(config.registered.includeIssueAtClaim)(now) + + if (config.registered.includeJwtIdClaim) + Option(decodedJWT.getId) should not be empty + else + Option(decodedJWT.getId) shouldBe empty + + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe config.registered.expiresAtOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe config.registered.notBeforeOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + } + + "successfully issue token with predefine configure claims and ad-hoc registered claims" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(registeredClaims.toClaims).value + + val expectedIssuer = registeredClaims.iss orElse config.registered.issuerClaim + val expectedSubject = registeredClaims.sub orElse config.registered.subjectClaim + val expectedAudience = + if (registeredClaims.aud.nonEmpty) registeredClaims.aud else config.registered.audienceClaims + val expectedIssuedAt = registeredClaims.iat orElse Option.when(config.registered.includeIssueAtClaim)(now) + val expectedExpiredAt = + registeredClaims.exp orElse config.registered.expiresAtOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + val expectedNotBefore = + registeredClaims.nbf orElse config.registered.notBeforeOffset.map(offset => + now.plusSeconds(offset.toSeconds) + ) + + jwtClaims.claims.registered.iss shouldBe expectedIssuer + jwtClaims.claims.registered.sub shouldBe expectedSubject + jwtClaims.claims.registered.aud shouldBe expectedAudience + jwtClaims.claims.registered.iat shouldBe expectedIssuedAt + jwtClaims.claims.registered.exp shouldBe expectedExpiredAt + jwtClaims.claims.registered.nbf shouldBe expectedNotBefore + + if (registeredClaims.jti.nonEmpty) + jwtClaims.claims.registered.jti shouldBe registeredClaims.jti + else if (config.registered.includeJwtIdClaim) + jwtClaims.claims.registered.jti should not be empty + else jwtClaims.claims.registered.jti shouldBe empty + } + + "successfully issue token with only registered claims empty strings" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val adHocRegisteredClaims = + registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value + + val decodedJWT = jwtVerifier.verify(jwtClaims.token) + + Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss + Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub + Option(decodedJWT.getAudience) + .map(_.asScala.toSeq) + .toSeq + .flatten shouldBe jwtClaims.claims.registered.aud + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat + Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } + + "successfully issue token with only registered claims when decoded should have the same values with the return registered claims" in forAll { + (registeredClaims: RegisteredClaims, config: JwtIssuerConfig) => + val now = getInstantNowSeconds + val adHocRegisteredClaims = + registeredClaims.copy(iat = Some(now), exp = Some(now.plusSeconds(5.minutes.toSeconds)), nbf = Some(now)) + val jwtIssuer = JwtIssuer(config, getFixedClock(now)) + val jwtClaims = jwtIssuer.issueJwt(adHocRegisteredClaims.toClaims).value + + val decodedJWT = jwtVerifier.verify(jwtClaims.token) + + Option(decodedJWT.getIssuer) shouldBe jwtClaims.claims.registered.iss + Option(decodedJWT.getSubject) shouldBe jwtClaims.claims.registered.sub + Option(decodedJWT.getAudience) + .map(_.asScala.toSeq) + .toSeq + .flatten shouldBe jwtClaims.claims.registered.aud + Try(decodedJWT.getIssuedAt.toInstant).toOption shouldBe jwtClaims.claims.registered.iat + Option(decodedJWT.getId) shouldBe jwtClaims.claims.registered.jti + Try(decodedJWT.getExpiresAt.toInstant).toOption shouldBe jwtClaims.claims.registered.exp + Try(decodedJWT.getNotBefore.toInstant).toOption shouldBe jwtClaims.claims.registered.nbf + } + + "fail with IllegalArgument when issue token algorithm is set to null" in forAll { (config: JwtIssuerConfig) => + val jwtIssuer = JwtIssuer(config.copy(algorithm = null)) + val jwt = jwtIssuer.issueJwt() - val result = jwtVerifier - .verify(jwt.token) - .pipe(_.getHeader) - .pipe(Base64.decodeToken) - .pipe(_.value) - .pipe(NestedHeader.claimsDecoder.decode) - .value + val signError = jwt.left.value + .asInstanceOf[JwtIssueError.SignError] - result shouldBe header + signError.message shouldBe "Signing token failed" + signError.getCause shouldBe a[IllegalArgumentException] + signError.getCause.getMessage shouldBe "The Algorithm cannot be null." + } } - "issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => - val jwtIssuer = JwtIssuer(config) - val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value - - val result = jwtVerifier - .verify(jwt.token) - .pipe(_.getPayload) - .pipe(Base64.decodeToken) - .pipe(_.value) - .pipe(NestedPayload.claimsDecoder.decode) - .value + "issueJwt ClaimsH" should { + "successfully issue token with header claims" in forAll { (config: JwtIssuerConfig, header: NestedHeader) => + val jwtIssuer = JwtIssuer(config) + val jwt = jwtIssuer.issueJwt(header.toClaimsH).value - result shouldBe payload + val result = jwtVerifier + .verify(jwt.token) + .pipe(_.getHeader) + .pipe(Base64.decodeToken) + .pipe(_.value) + .pipe(NestedHeader.claimsDecoder.decode) + .value + + result shouldBe header + } } - "issue token with header & payload claims" in forAll { - (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => + "issueJwt ClaimsP" should { + "successfully issue token with payload claims" in forAll { (config: JwtIssuerConfig, payload: NestedPayload) => val jwtIssuer = JwtIssuer(config) - val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value + val jwt = jwtIssuer.issueJwt(payload.toClaimsP).value - val (headerResult, payloadResult) = jwtVerifier + val result = jwtVerifier .verify(jwt.token) - .pipe(decodedJwt => - Base64.decodeToken(decodedJwt.getHeader).value -> Base64.decodeToken(decodedJwt.getPayload).value - ) - .pipe { case (headerJson, payloadJson) => - (NestedHeader.claimsDecoder.decode(headerJson).value, NestedPayload.claimsDecoder.decode(payloadJson).value) - } - - headerResult shouldBe header - payloadResult shouldBe payload + .pipe(_.getPayload) + .pipe(Base64.decodeToken) + .pipe(_.value) + .pipe(NestedPayload.claimsDecoder.decode) + .value + + result shouldBe payload + } } - "issue token should fail with IllegalArgument when algorithm is set to null" in forAll { - (config: JwtIssuerConfig) => - val jwtIssuer = JwtIssuer(config.copy(algorithm = null)) - val jwt = jwtIssuer.issueJwt() - - val signError = jwt.left.value - .asInstanceOf[JwtIssueError.SignError] - - signError.message shouldBe "Signing token failed" - signError.getCause shouldBe a[IllegalArgumentException] - signError.getCause.getMessage shouldBe "The Algorithm cannot be null." + "issueJwt ClaimsHP" should { + "successfully issue token with header & payload claims" in forAll { + (config: JwtIssuerConfig, header: NestedHeader, payload: NestedPayload) => + val jwtIssuer = JwtIssuer(config) + val jwt = jwtIssuer.issueJwt((header, payload).toClaimsHP).value + + val (headerResult, payloadResult) = jwtVerifier + .verify(jwt.token) + .pipe(decodedJwt => + Base64.decodeToken(decodedJwt.getHeader).value -> Base64.decodeToken(decodedJwt.getPayload).value + ) + .pipe { case (headerJson, payloadJson) => + ( + NestedHeader.claimsDecoder.decode(headerJson).value, + NestedPayload.claimsDecoder.decode(payloadJson).value, + ) + } + + headerResult shouldBe header + payloadResult shouldBe payload + } } } } diff --git a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala index 92a271e..6470a1a 100644 --- a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -49,271 +49,285 @@ class JwtVerifierSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { TestData(registeredClaims, builderWithRegistered) } - "JwtVerifier" should { - "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config) - val testData = setRegisteredClaims(JWT.create(), config) - val token = testData.builder.sign(config.algorithm) + "JwtVerifier" when { + "verifyJwt Token" should { + "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder.sign(config.algorithm) - val verified = jwtVerifier.verifyJwt(token.toToken).value + val verified = jwtVerifier.verifyJwt(token.toToken).value - verified.registered shouldBe testData.registeredClaims - } + verified.registered shouldBe testData.registeredClaims + } - "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config) - val testData = setRegisteredClaims(JWT.create(), config) - val token = testData.builder - .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) - .sign(config.algorithm) + "fail to verify token with VerificationError when provided with claims are not meet criteria" in { + val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) + val jwtVerifier = JwtVerifier(config) + val token = JWT + .create() + .sign(config.algorithm) - val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] - verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) - } + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[JWTVerificationException] + verificationError.underlying.value.getMessage shouldBe "The Claim 'iss' is not present in the JWT." + } + + "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = null)) + val token = JWT + .create() + .sign(config.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[IllegalArgumentException] + verificationError.underlying.value.getMessage shouldBe "The Algorithm cannot be null." + } + + "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"))) + val token = JWT + .create() + .sign(config.algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[AlgorithmMismatchException] + verificationError.underlying.value.getMessage shouldBe "The provided Algorithm doesn't match the one defined in the JWT's Header." + } + + "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { + (config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"))) + val algorithm = Algorithm.HMAC256("secret1") + val token = JWT + .create() + .sign(algorithm) + + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] + + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[SignatureVerificationException] + verificationError.underlying.value.getMessage shouldBe "The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256" + } + + "fail to verify token with TokenExpired when JWT expires" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val expiresAt = getInstantNowSeconds.minusSeconds(1) + val token = JWT + .create() + .withExpiresAt(expiresAt) + .sign(defaultConfig.algorithm) - "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config) - val testData = setRegisteredClaims(JWT.create(), config) - val token = testData.builder - .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) - .sign(config.algorithm) + val verificationError = jwtVerifier + .verifyJwt(token.toToken) + .left + .value + .asInstanceOf[JwtVerifyError.VerificationError] - val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) + verificationError.message shouldBe "JwtVerifier failed with verification error" + verificationError.underlying.value shouldBe a[TokenExpiredException] + verificationError.underlying.value.getMessage shouldBe s"The Token has expired on $expiresAt." + } + + "fail to verify an empty string token" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = "" + val verified = jwtVerifier.verifyJwt(token.toToken) + val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) + val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + + val verificationError = verified.left.value + .asInstanceOf[JwtVerifyError.VerificationError] - verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) - } + verificationError.message shouldBe "JWTVerifier failed with an empty token." + verificationError.underlying shouldBe empty - "verify a token with header & payload" in forAll { - (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config) - val testData = setRegisteredClaims(JWT.create(), config) - val token = testData.builder - .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) - .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) - .sign(config.algorithm) + val verificationErrorH = verifiedH.left.value + .asInstanceOf[JwtVerifyError.VerificationError] - val verified = - jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + verificationErrorH.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorH.underlying shouldBe empty - verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) - } + val verificationErrorP = verifiedP.left.value + .asInstanceOf[JwtVerifyError.VerificationError] - "fail to decode a token with header" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val header = """{"name": "name"}""" - val token = JWT - .create() - .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) - .sign(defaultConfig.algorithm) + verificationErrorP.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorP.underlying shouldBe empty - val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) + val verificationErrorHP = verifiedHP.left.value + .asInstanceOf[JwtVerifyError.VerificationError] - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) + verificationErrorHP.message shouldBe "JWTVerifier failed with an empty token." + verificationErrorHP.underlying shouldBe empty + } } - "fail to decode a token with payload" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val payload = """{"name": "name"}""" - val token = JWT - .create() - .withPayload(CodecHelper.unsafeParseJsonToJavaMap(payload)) - .sign(defaultConfig.algorithm) - - val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) - } + "verifyJwt TokenH" should { + "verify a token with header" in forAll { (nestedHeader: NestedHeader, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) + .sign(config.algorithm) - "fail to decode a token with header & payload" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val header = """{"name": "name"}""" - val token = JWT - .create() - .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) - .sign(defaultConfig.algorithm) + val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - val verified = - jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + verified.value shouldBe nestedHeader.toClaimsH.copy(registered = testData.registeredClaims) + } - verified shouldBe Left(JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null)) - } + "fail to decode a token with header" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val header = """{"name": "name"}""" + val token = JWT + .create() + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) + .sign(defaultConfig.algorithm) - "fail to decode a token with header if exception raised in decoder" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val token = JWT - .create() - .sign(defaultConfig.algorithm) - - val decodingError = jwtVerifier - .verifyJwt[SimpleHeader](token.toTokenH) - .left - .value - .asInstanceOf[JwtVerifyError.DecodingError] - - decodingError.message shouldBe "Boom" - decodingError.underlying shouldBe a[RuntimeException] - decodingError.underlying.getMessage shouldBe "Boom" - } + val verified = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - "fail to decode a token with payload if exception raised in decoder" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val token = JWT - .create() - .sign(defaultConfig.algorithm) - - val decodingError = jwtVerifier - .verifyJwt[SimplePayload](token.toTokenP) - .left - .value - .asInstanceOf[JwtVerifyError.DecodingError] - - decodingError.message shouldBe "Boom" - decodingError.underlying shouldBe a[RuntimeException] - decodingError.underlying.getMessage shouldBe "Boom" - } + verified shouldBe Left( + JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null) + ) + } - "fail to decode a token with header & payload if exception raised in decoder" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val token = JWT - .create() - .sign(defaultConfig.algorithm) + "fail to decode a token with header if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = JWT + .create() + .sign(defaultConfig.algorithm) - val decodingError = - jwtVerifier - .verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) + val decodingError = jwtVerifier + .verifyJwt[SimpleHeader](token.toTokenH) .left .value .asInstanceOf[JwtVerifyError.DecodingError] - decodingError.message shouldBe "Boom" - decodingError.underlying shouldBe a[RuntimeException] - decodingError.underlying.getMessage shouldBe "Boom" - } - - "fail to verify token with VerificationError when provided with claims are not meet criteria" in { - val config = defaultConfig.copy(providedWith = defaultConfig.providedWith.copy(issuerClaim = Some("issuer"))) - val jwtVerifier = JwtVerifier(config) - val token = JWT - .create() - .sign(config.algorithm) - - val verificationError = jwtVerifier - .verifyJwt(token.toToken) - .left - .value - .asInstanceOf[JwtVerifyError.VerificationError] - - verificationError.message shouldBe "JwtVerifier failed with verification error" - verificationError.underlying.value shouldBe a[JWTVerificationException] - verificationError.underlying.value.getMessage shouldBe "The Claim 'iss' is not present in the JWT." + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } } - "fail to verify token with IllegalArgument when null algorithm is provided" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config.copy(algorithm = null)) - val token = JWT - .create() + "verifyJwt TokenP" should { + "verify a token with payload" in forAll { (nestedPayload: NestedPayload, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) .sign(config.algorithm) - val verificationError = jwtVerifier - .verifyJwt(token.toToken) - .left - .value - .asInstanceOf[JwtVerifyError.VerificationError] + val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - verificationError.message shouldBe "JwtVerifier failed with verification error" - verificationError.underlying.value shouldBe a[IllegalArgumentException] - verificationError.underlying.value.getMessage shouldBe "The Algorithm cannot be null." - } + verified.value shouldBe nestedPayload.toClaimsP.copy(registered = testData.registeredClaims) + } - "fail to verify token with AlgorithmMismatch when jwt header algorithm doesn't match with verify" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret"))) + "fail to decode a token with payload" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val payload = """{"name": "name"}""" val token = JWT .create() - .sign(config.algorithm) + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(payload)) + .sign(defaultConfig.algorithm) - val verificationError = jwtVerifier - .verifyJwt(token.toToken) - .left - .value - .asInstanceOf[JwtVerifyError.VerificationError] + val verified = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - verificationError.message shouldBe "JwtVerifier failed with verification error" - verificationError.underlying.value shouldBe a[AlgorithmMismatchException] - verificationError.underlying.value.getMessage shouldBe "The provided Algorithm doesn't match the one defined in the JWT's Header." - } + verified shouldBe Left( + JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null) + ) + } - "fail to verify token with SignatureVerificationError when secrets provided are wrong" in forAll { - (config: JwtVerifierConfig) => - val jwtVerifier = JwtVerifier(config.copy(algorithm = Algorithm.HMAC256("secret2"))) - val algorithm = Algorithm.HMAC256("secret1") + "fail to decode a token with payload if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) val token = JWT .create() - .sign(algorithm) + .sign(defaultConfig.algorithm) - val verificationError = jwtVerifier - .verifyJwt(token.toToken) + val decodingError = jwtVerifier + .verifyJwt[SimplePayload](token.toTokenP) .left .value - .asInstanceOf[JwtVerifyError.VerificationError] - - verificationError.message shouldBe "JwtVerifier failed with verification error" - verificationError.underlying.value shouldBe a[SignatureVerificationException] - verificationError.underlying.value.getMessage shouldBe "The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256" - } + .asInstanceOf[JwtVerifyError.DecodingError] - "fail to verify token with TokenExpired when JWT expires" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val expiresAt = getInstantNowSeconds.minusSeconds(1) - val token = JWT - .create() - .withExpiresAt(expiresAt) - .sign(defaultConfig.algorithm) - - val verificationError = jwtVerifier - .verifyJwt(token.toToken) - .left - .value - .asInstanceOf[JwtVerifyError.VerificationError] - - verificationError.message shouldBe "JwtVerifier failed with verification error" - verificationError.underlying.value shouldBe a[TokenExpiredException] - verificationError.underlying.value.getMessage shouldBe s"The Token has expired on $expiresAt." + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } } - "fail to verify an empty string token" in { - val jwtVerifier = JwtVerifier(defaultConfig) - val token = "" - val verified = jwtVerifier.verifyJwt(token.toToken) - val verifiedH = jwtVerifier.verifyJwt[NestedHeader](token.toTokenH) - val verifiedP = jwtVerifier.verifyJwt[NestedPayload](token.toTokenP) - val verifiedHP = jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - - val verificationError = verified.left.value - .asInstanceOf[JwtVerifyError.VerificationError] - - verificationError.message shouldBe "JWTVerifier failed with an empty token." - verificationError.underlying shouldBe empty - - val verificationErrorH = verifiedH.left.value - .asInstanceOf[JwtVerifyError.VerificationError] - - verificationErrorH.message shouldBe "JWTVerifier failed with an empty token." - verificationErrorH.underlying shouldBe empty - - val verificationErrorP = verifiedP.left.value - .asInstanceOf[JwtVerifyError.VerificationError] + "verifyJwt TokenHP" should { + "verify a token with header & payload" in forAll { + (nestedHeader: NestedHeader, nestedPayload: NestedPayload, config: JwtVerifierConfig) => + val jwtVerifier = JwtVerifier(config) + val testData = setRegisteredClaims(JWT.create(), config) + val token = testData.builder + .withPayload(CodecHelper.unsafeParseJsonToJavaMap(NestedPayload.claimsEncoder.encode(nestedPayload))) + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(NestedHeader.claimsEncoder.encode(nestedHeader))) + .sign(config.algorithm) + + val verified = + jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) + + verified.value shouldBe (nestedHeader, nestedPayload).toClaimsHP.copy(registered = testData.registeredClaims) + } + + "fail to decode a token with header & payload" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val header = """{"name": "name"}""" + val token = JWT + .create() + .withHeader(CodecHelper.unsafeParseJsonToJavaMap(header)) + .sign(defaultConfig.algorithm) - verificationErrorP.message shouldBe "JWTVerifier failed with an empty token." - verificationErrorP.underlying shouldBe empty + val verified = + jwtVerifier.verifyJwt[NestedHeader, NestedPayload](token.toTokenHP) - val verificationErrorHP = verifiedHP.left.value - .asInstanceOf[JwtVerifyError.VerificationError] + verified shouldBe Left( + JwtVerifyError.DecodingError("DecodingFailure at .mapping: Missing required field", null) + ) + } - verificationErrorHP.message shouldBe "JWTVerifier failed with an empty token." - verificationErrorHP.underlying shouldBe empty + "fail to decode a token with header & payload if exception raised in decoder" in { + val jwtVerifier = JwtVerifier(defaultConfig) + val token = JWT + .create() + .sign(defaultConfig.algorithm) + + val decodingError = + jwtVerifier + .verifyJwt[SimpleHeader, SimplePayload](token.toTokenHP) + .left + .value + .asInstanceOf[JwtVerifyError.DecodingError] + + decodingError.message shouldBe "Boom" + decodingError.underlying shouldBe a[RuntimeException] + decodingError.underlying.getMessage shouldBe "Boom" + } } } } From 20e2bd33c408f321d7e38f1cf378439093f27f6f Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sat, 8 Feb 2025 14:07:49 +0000 Subject: [PATCH 14/15] feat: update plugin --- .github/workflows/ci.yml | 33 +++++++++++++++++++++------------ build.sbt | 4 ++-- project/build.properties | 2 +- project/plugins.sbt | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e74c26f..520e951 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ concurrency: jobs: build: - name: Build and Test + name: Test strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] scala: [3] java: [temurin@11, temurin@17, temurin@21] runs-on: ${{ matrix.os }} @@ -40,6 +40,9 @@ jobs: with: fetch-depth: 0 + - name: Setup sbt + uses: sbt/setup-sbt@v1 + - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' @@ -86,11 +89,11 @@ jobs: run: sbt '++ ${{ matrix.scala }}' test - name: Check binary compatibility - if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-latest' + if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04' run: sbt '++ ${{ matrix.scala }}' mimaReportBinaryIssues - name: Generate API documentation - if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-latest' + if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04' run: sbt '++ ${{ matrix.scala }}' doc - name: Make target directories @@ -114,7 +117,7 @@ jobs: if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -123,6 +126,9 @@ jobs: with: fetch-depth: 0 + - name: Setup sbt + uses: sbt/setup-sbt@v1 + - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' @@ -198,10 +204,10 @@ jobs: dependency-submission: name: Submit Dependencies - if: github.event_name != 'pull_request' + if: github.event.repository.fork == false && github.event_name != 'pull_request' strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -210,6 +216,9 @@ jobs: with: fetch-depth: 0 + - name: Setup sbt + uses: sbt/setup-sbt@v1 + - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' @@ -259,7 +268,7 @@ jobs: name: Validate Steward Config strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -284,8 +293,8 @@ jobs: name: Check code style strategy: matrix: - os: [ubuntu-latest] - scala: [3.3.4] + os: [ubuntu-22.04] + scala: [3.3.5] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -312,8 +321,8 @@ jobs: name: Codecov strategy: matrix: - os: [ubuntu-latest] - scala: [3.3.4] + os: [ubuntu-22.04] + scala: [3.3.5] java: [temurin@11] runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index 16766cd..4b6bb74 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import org.typelevel.sbt.gha.Permissions Global / onChangedBuildSource := ReloadOnSourceChanges -ThisBuild / scalaVersion := "3.3.4" +ThisBuild / scalaVersion := "3.3.5" ThisBuild / organization := "io.github.scala-jwt" ThisBuild / organizationName := "oath" ThisBuild / organizationHomepage := Some(url("https://github.com/scala-jwt/oath")) @@ -12,7 +12,7 @@ ThisBuild / licenses := Seq(License.Apache2) ThisBuild / developers := List( tlGitHubDev("andrewrigas", "Andreas Rigas") ) -ThisBuild / tlSonatypeUseLegacyHost := false +ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy ThisBuild / startYear := Some(2022) ThisBuild / githubWorkflowPermissions := Some(Permissions.WriteAll) ThisBuild / githubWorkflowJavaVersions := Seq("11", "17", "21").map(JavaSpec.temurin) diff --git a/project/build.properties b/project/build.properties index 136f452..fe69360 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.10.1 +sbt.version = 1.10.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index c3b53b9..a52a6f3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,4 +2,4 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0") -addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.6.7") +addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.7.7") From ce41663d5542f9bc535a454cbe240ae67e295227 Mon Sep 17 00:00:00 2001 From: Andreas Rigas Date: Sun, 16 Mar 2025 14:08:06 +0000 Subject: [PATCH 15/15] feat: test coverage --- build.sbt | 1 + .../main/scala/io/oath/circe/conversion.scala | 28 ++++++++++++---- .../src/main/scala/io/oath/circe/syntax.scala | 32 ------------------- .../src/test/scala/io/oath/circe/Bar.scala | 2 +- .../io/oath/circe/CirceConversionSpec.scala | 8 ++--- .../src/test/scala/io/oath/circe/Foo.scala | 2 +- .../src/main/scala/io/oath/JwtIssuer.scala | 6 ++-- .../src/main/scala/io/oath/JwtManager.scala | 2 +- .../src/main/scala/io/oath/JwtVerifier.scala | 2 +- .../main/scala/io/oath/JwtVerifyError.scala | 2 +- .../src/main/scala/io/oath/OathManager.scala | 2 +- .../src/main/scala/io/oath/OathVerifier.scala | 2 +- .../io/oath/config/AlgorithmLoader.scala | 4 +-- .../io/oath/config/JwtVerifierConfig.scala | 14 ++++---- .../main/scala/io/oath/config/package.scala | 6 ++-- .../src/main/scala/io/oath/utils/Base64.scala | 2 +- oath/core/src/test/resources/issuer.conf | 12 +++++++ oath/core/src/test/resources/verifier.conf | 14 +++++++- .../test/scala/io/oath/JwtIssuerSpec.scala | 12 +++---- .../test/scala/io/oath/JwtManagerSpec.scala | 6 ++-- .../test/scala/io/oath/JwtVerifierSpec.scala | 10 +++--- .../src/test/scala/io/oath/NestedHeader.scala | 10 +++--- .../test/scala/io/oath/NestedPayload.scala | 8 ++--- .../test/scala/io/oath/OathManagerSpec.scala | 2 +- .../test/scala/io/oath/OathVerifierSpec.scala | 2 +- .../io/oath/config/AlgorithmLoaderSpec.scala | 2 +- .../io/oath/config/JwtIssuerLoaderSpec.scala | 9 +++++- .../io/oath/config/JwtManagerLoaderSpec.scala | 2 +- .../oath/config/JwtVerifierLoaderSpec.scala | 9 +++++- .../scala/io/oath/testkit/Arbitraries.scala | 12 +++---- .../io/oath/jsoniter_scala/conversion.scala | 23 ++++++++++--- .../scala/io/oath/jsoniter_scala/syntax.scala | 20 ------------ .../scala/io/oath/jsoniter_scala/Bar.scala | 11 +++---- .../JsoniterConversionSpec.scala | 10 +++--- project/Dependencies.scala | 10 +++--- project/plugins.sbt | 8 ++--- 36 files changed, 161 insertions(+), 146 deletions(-) delete mode 100644 oath/circe/src/main/scala/io/oath/circe/syntax.scala delete mode 100644 oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala diff --git a/build.sbt b/build.sbt index 4b6bb74..cb59cc1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ import org.typelevel.sbt.gha.Permissions +//import org.typelevel.scalacoptions.ScalacOptions Global / onChangedBuildSource := ReloadOnSourceChanges diff --git a/oath/circe/src/main/scala/io/oath/circe/conversion.scala b/oath/circe/src/main/scala/io/oath/circe/conversion.scala index 7a033ba..3963b72 100644 --- a/oath/circe/src/main/scala/io/oath/circe/conversion.scala +++ b/oath/circe/src/main/scala/io/oath/circe/conversion.scala @@ -1,13 +1,29 @@ package io.oath.circe -import io.circe.* -import io.oath.circe.syntax.* -import io.oath.json.* +import io.circe._ +import io.circe.syntax.EncoderOps +import io.oath.JwtVerifyError +import io.oath.json._ object conversion { - given [P](using encoder: Encoder[P]): ClaimsEncoder[P] = encoder.convert + given [P](using codec: Codec[P]): ClaimsCodec[P] = new ClaimsCodec[P] { + override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = + decoderConverter[P].decode(token) - given [P](using decoder: Decoder[P]): ClaimsDecoder[P] = decoder.convert + override def encode(data: P): String = + encoderConverter[P].encode(data) + } - given [P](using codec: Codec[P]): ClaimsCodec[P] = codec.convertCodec + given encoderConverter[P](using encoder: Encoder[P]): ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces + + given decoderConverter[P](using decoder: Decoder[P]): ClaimsDecoder[P] = json => + parser + .parse(json) + .left + .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) + .flatMap( + _.as[P](decoder).left.map(decodingFailure => + JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) + ) + ) } diff --git a/oath/circe/src/main/scala/io/oath/circe/syntax.scala b/oath/circe/src/main/scala/io/oath/circe/syntax.scala deleted file mode 100644 index 05e95d9..0000000 --- a/oath/circe/src/main/scala/io/oath/circe/syntax.scala +++ /dev/null @@ -1,32 +0,0 @@ -package io.oath.circe - -import io.circe.* -import io.circe.syntax.EncoderOps -import io.oath.JwtVerifyError -import io.oath.json.{ClaimsCodec, ClaimsDecoder, ClaimsEncoder} - -object syntax { - extension [P](encoder: Encoder[P]) def convert: ClaimsEncoder[P] = data => data.asJson(encoder).noSpaces - - extension [P](decoder: Decoder[P]) - def convert: ClaimsDecoder[P] = - json => - parser - .parse(json) - .left - .map(parsingFailure => JwtVerifyError.DecodingError(parsingFailure.message, parsingFailure.underlying)) - .flatMap( - _.as[P](decoder).left.map(decodingFailure => - JwtVerifyError.DecodingError(decodingFailure.getMessage(), decodingFailure.getCause) - ) - ) - - extension [P](codec: Codec[P]) - def convertCodec: ClaimsCodec[P] = new ClaimsCodec[P] { - override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = - codec.asInstanceOf[Decoder[P]].convert.decode(token) - - override def encode(data: P): String = - codec.asInstanceOf[Encoder[P]].convert.encode(data) - } -} diff --git a/oath/circe/src/test/scala/io/oath/circe/Bar.scala b/oath/circe/src/test/scala/io/oath/circe/Bar.scala index e1ead9c..85a4ca1 100644 --- a/oath/circe/src/test/scala/io/oath/circe/Bar.scala +++ b/oath/circe/src/test/scala/io/oath/circe/Bar.scala @@ -1,6 +1,6 @@ package io.oath.circe -import io.circe.generic.semiauto.* +import io.circe.generic.semiauto._ import io.circe.{Decoder, Encoder} final case class Bar(name: String, age: Int) diff --git a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala index d388a12..4eadf57 100644 --- a/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala +++ b/oath/circe/src/test/scala/io/oath/circe/CirceConversionSpec.scala @@ -2,15 +2,15 @@ package io.oath.circe import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.oath.* +import io.oath._ import io.oath.circe.conversion.given import io.oath.config.JwtIssuerConfig.RegisteredConfig import io.oath.config.JwtVerifierConfig.{LeewayWindowConfig, ProvidedWithConfig} -import io.oath.config.* +import io.oath.config._ import io.oath.json.ClaimsDecoder -import io.oath.syntax.all.* +import io.oath.syntax.all._ import io.oath.testkit.CodecHelper.unsafeParseJsonToJavaMap -import io.oath.testkit.* +import io.oath.testkit._ import org.typelevel.jawn.ParseException class CirceConversionSpec extends WordSpecBase { diff --git a/oath/circe/src/test/scala/io/oath/circe/Foo.scala b/oath/circe/src/test/scala/io/oath/circe/Foo.scala index d39ed16..85bc80f 100644 --- a/oath/circe/src/test/scala/io/oath/circe/Foo.scala +++ b/oath/circe/src/test/scala/io/oath/circe/Foo.scala @@ -1,7 +1,7 @@ package io.oath.circe import io.circe.Codec -import io.circe.generic.semiauto.* +import io.circe.generic.semiauto._ final case class Foo(name: String, age: Int) diff --git a/oath/core/src/main/scala/io/oath/JwtIssuer.scala b/oath/core/src/main/scala/io/oath/JwtIssuer.scala index d6833c2..eac930a 100644 --- a/oath/core/src/main/scala/io/oath/JwtIssuer.scala +++ b/oath/core/src/main/scala/io/oath/JwtIssuer.scala @@ -2,17 +2,17 @@ package io.oath import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.{JWT, JWTCreator} -import io.oath.config.* +import io.oath.config._ import io.oath.json.ClaimsEncoder import java.time.temporal.ChronoUnit import java.time.{Clock, Instant} import java.util.UUID -import scala.util.chaining.* +import scala.util.chaining._ import scala.util.control.Exception.allCatch trait JwtIssuer { - def issueJwt(claims: JwtClaims.Claims = JwtClaims.Claims()): Either[JwtIssueError, Jwt[JwtClaims.Claims]] + def issueJwt(claims: JwtClaims.Claims): Either[JwtIssueError, Jwt[JwtClaims.Claims]] def issueJwt[H](claims: JwtClaims.ClaimsH[H])(using ClaimsEncoder[H] ): Either[JwtIssueError, Jwt[JwtClaims.ClaimsH[H]]] diff --git a/oath/core/src/main/scala/io/oath/JwtManager.scala b/oath/core/src/main/scala/io/oath/JwtManager.scala index ca62989..956ab10 100644 --- a/oath/core/src/main/scala/io/oath/JwtManager.scala +++ b/oath/core/src/main/scala/io/oath/JwtManager.scala @@ -1,6 +1,6 @@ package io.oath -import io.oath.config.* +import io.oath.config._ import io.oath.json.{ClaimsDecoder, ClaimsEncoder} import java.time.Clock diff --git a/oath/core/src/main/scala/io/oath/JwtVerifier.scala b/oath/core/src/main/scala/io/oath/JwtVerifier.scala index 65d251d..cca4308 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifier.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifier.scala @@ -3,7 +3,7 @@ package io.oath import com.auth0.jwt.JWT import com.auth0.jwt.interfaces.DecodedJWT import io.oath.config.JwtVerifierConfig -import io.oath.json.* +import io.oath.json._ import io.oath.utils.Base64 import scala.util.chaining.scalaUtilChainingOps diff --git a/oath/core/src/main/scala/io/oath/JwtVerifyError.scala b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala index c435096..f78895e 100644 --- a/oath/core/src/main/scala/io/oath/JwtVerifyError.scala +++ b/oath/core/src/main/scala/io/oath/JwtVerifyError.scala @@ -1,6 +1,6 @@ package io.oath -import cats.syntax.all.* +import cats.syntax.all._ sealed abstract class JwtVerifyError(error: String, cause: Option[Throwable] = None) extends Exception(error, cause.orNull) diff --git a/oath/core/src/main/scala/io/oath/OathManager.scala b/oath/core/src/main/scala/io/oath/OathManager.scala index 76d7bee..b896105 100644 --- a/oath/core/src/main/scala/io/oath/OathManager.scala +++ b/oath/core/src/main/scala/io/oath/OathManager.scala @@ -1,7 +1,7 @@ package io.oath import io.oath.OathManager.JManager -import io.oath.config.* +import io.oath.config._ import io.oath.macros.OathEnum import scala.util.chaining.scalaUtilChainingOps diff --git a/oath/core/src/main/scala/io/oath/OathVerifier.scala b/oath/core/src/main/scala/io/oath/OathVerifier.scala index 71f4d97..84194fd 100644 --- a/oath/core/src/main/scala/io/oath/OathVerifier.scala +++ b/oath/core/src/main/scala/io/oath/OathVerifier.scala @@ -1,7 +1,7 @@ package io.oath import io.oath.OathVerifier.JVerifier -import io.oath.config.* +import io.oath.config._ import io.oath.macros.OathEnum import scala.util.chaining.scalaUtilChainingOps diff --git a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala index 04eb688..998686a 100644 --- a/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala +++ b/oath/core/src/main/scala/io/oath/config/AlgorithmLoader.scala @@ -5,11 +5,11 @@ import com.typesafe.config.Config import org.bouncycastle.util.io.pem.PemReader import java.io.{File, FileReader} -import java.security.interfaces.* +import java.security.interfaces._ import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} import java.security.{KeyFactory, PrivateKey, PublicKey} import scala.util.Using -import scala.util.chaining.* +import scala.util.chaining._ object AlgorithmLoader { private val SecretKeyConfigValue = "secret-key" diff --git a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala index ee1f914..b151991 100644 --- a/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala +++ b/oath/core/src/main/scala/io/oath/config/JwtVerifierConfig.scala @@ -2,7 +2,7 @@ package io.oath.config import com.auth0.jwt.algorithms.Algorithm import com.typesafe.config.{Config, ConfigFactory} -import io.oath.config.JwtVerifierConfig.* +import io.oath.config.JwtVerifierConfig._ import scala.concurrent.duration.FiniteDuration @@ -50,21 +50,21 @@ object JwtVerifierConfig { def none(): JwtVerifierConfig = JwtVerifierConfig(Algorithm.none(), ProvidedWithConfig(), LeewayWindowConfig()) def loadOrThrow(config: Config): JwtVerifierConfig = - (for + (for { algorithmScoped <- config.getMaybeConfig(AlgorithmConfigLocation) algorithmConfig = AlgorithmLoader.loadOrThrow(algorithmScoped, isIssuer = false) maybeVerificationScoped = config.getMaybeConfig(VerifierConfigLocation) maybeProvidedWithConfig = - for + for { verificationScoped <- maybeVerificationScoped providedWithScoped <- verificationScoped.getMaybeConfig(ProvidedWithConfigLocation) - yield loadOrdThrowProvidedWithConfig(providedWithScoped) + } yield loadOrdThrowProvidedWithConfig(providedWithScoped) maybeLeewayWindowConfig = - for + for { verificationScoped <- maybeVerificationScoped leewayWindowScoped <- verificationScoped.getMaybeConfig(LeewayWindowConfigLocation) - yield loadOrThrowLeewayWindowConfig(leewayWindowScoped) - yield JwtVerifierConfig( + } yield loadOrThrowLeewayWindowConfig(leewayWindowScoped) + } yield JwtVerifierConfig( algorithmConfig, maybeProvidedWithConfig.getOrElse(ProvidedWithConfig()), maybeLeewayWindowConfig.getOrElse(LeewayWindowConfig()), diff --git a/oath/core/src/main/scala/io/oath/config/package.scala b/oath/core/src/main/scala/io/oath/config/package.scala index 3fe748e..20b37c5 100644 --- a/oath/core/src/main/scala/io/oath/config/package.scala +++ b/oath/core/src/main/scala/io/oath/config/package.scala @@ -3,9 +3,9 @@ package io.oath.config import com.typesafe.config.{Config, ConfigException, ConfigFactory} import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters.* -import scala.jdk.DurationConverters.* -import scala.util.chaining.* +import scala.jdk.CollectionConverters._ +import scala.jdk.DurationConverters._ +import scala.util.chaining._ import scala.util.control.Exception.allCatch private[config] val OathLocation = "oath" diff --git a/oath/core/src/main/scala/io/oath/utils/Base64.scala b/oath/core/src/main/scala/io/oath/utils/Base64.scala index ac3d151..3a77047 100644 --- a/oath/core/src/main/scala/io/oath/utils/Base64.scala +++ b/oath/core/src/main/scala/io/oath/utils/Base64.scala @@ -3,7 +3,7 @@ package io.oath.utils import io.oath.JwtVerifyError import java.nio.charset.StandardCharsets -import java.util.Base64 as JBase64 +import java.util.{Base64 => JBase64} import scala.util.control.Exception.allCatch private[oath] object Base64 { diff --git a/oath/core/src/test/resources/issuer.conf b/oath/core/src/test/resources/issuer.conf index 87e1b16..be3138f 100644 --- a/oath/core/src/test/resources/issuer.conf +++ b/oath/core/src/test/resources/issuer.conf @@ -64,3 +64,15 @@ invalid-token-wrong-type { } } } + +invalid-seq-empty-string { + algorithm { + name = "HS256" + secret-key = "secret" + } + issuer { + registered { + audience-claims = ["aud1", ""] + } + } +} diff --git a/oath/core/src/test/resources/verifier.conf b/oath/core/src/test/resources/verifier.conf index b192465..b87fc6f 100644 --- a/oath/core/src/test/resources/verifier.conf +++ b/oath/core/src/test/resources/verifier.conf @@ -30,7 +30,7 @@ without-public-key-token { name = "RS256" private-key-pem-path = "src/test/secrets/rsa-public.pem" } - + verifier { provided-with { issuer-claim = "issuer" @@ -69,3 +69,15 @@ invalid-token-wrong-type { } } } + +invalid-seq-empty-string { + algorithm { + name = "HS256" + secret-key = "secret" + } + verifier { + provided-with { + audience-claims = ["aud1", ""] + } + } +} diff --git a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala index b604d08..0c27644 100644 --- a/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtIssuerSpec.scala @@ -2,15 +2,15 @@ package io.oath import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.oath.config.* -import io.oath.syntax.all.* -import io.oath.testkit.* -import io.oath.utils.* +import io.oath.config._ +import io.oath.syntax.all._ +import io.oath.testkit._ +import io.oath.utils._ -import scala.concurrent.duration.* +import scala.concurrent.duration._ import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.Try -import scala.util.chaining.* +import scala.util.chaining._ class JwtIssuerSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { diff --git a/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala index 8db6ed0..3de90c4 100644 --- a/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtManagerSpec.scala @@ -1,8 +1,8 @@ package io.oath -import io.oath.config.* -import io.oath.syntax.all.* -import io.oath.testkit.* +import io.oath.config._ +import io.oath.syntax.all._ +import io.oath.testkit._ class JwtManagerSpec extends WordSpecBase, PropertyBasedTesting { diff --git a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala index 6470a1a..3062462 100644 --- a/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/JwtVerifierSpec.scala @@ -1,14 +1,14 @@ package io.oath import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.* +import com.auth0.jwt.exceptions._ import com.auth0.jwt.{JWT, JWTCreator} import io.oath.NestedHeader.SimpleHeader import io.oath.NestedPayload.SimplePayload import io.oath.config.JwtVerifierConfig -import io.oath.config.JwtVerifierConfig.* -import io.oath.syntax.all.* -import io.oath.testkit.* +import io.oath.config.JwtVerifierConfig._ +import io.oath.syntax.all._ +import io.oath.testkit._ import scala.util.chaining.scalaUtilChainingOps @@ -50,7 +50,7 @@ class JwtVerifierSpec extends WordSpecBase, PropertyBasedTesting, ClockHelper { } "JwtVerifier" when { - "verifyJwt Token" should { + "verifyJwt" should { "verify token with prerequisite configurations" in forAll { (config: JwtVerifierConfig) => val jwtVerifier = JwtVerifier(config) val testData = setRegisteredClaims(JWT.create(), config) diff --git a/oath/core/src/test/scala/io/oath/NestedHeader.scala b/oath/core/src/test/scala/io/oath/NestedHeader.scala index 53edb21..1c0175f 100644 --- a/oath/core/src/test/scala/io/oath/NestedHeader.scala +++ b/oath/core/src/test/scala/io/oath/NestedHeader.scala @@ -1,11 +1,11 @@ package io.oath -import io.circe.* -import io.circe.generic.semiauto.* -import io.circe.parser.* -import io.circe.syntax.* +import io.circe._ +import io.circe.generic.semiauto._ +import io.circe.parser._ +import io.circe.syntax._ import io.oath.NestedHeader.SimpleHeader -import io.oath.json.* +import io.oath.json._ final case class NestedHeader(name: String, mapping: Map[String, SimpleHeader]) diff --git a/oath/core/src/test/scala/io/oath/NestedPayload.scala b/oath/core/src/test/scala/io/oath/NestedPayload.scala index ed3710d..ff4cd53 100644 --- a/oath/core/src/test/scala/io/oath/NestedPayload.scala +++ b/oath/core/src/test/scala/io/oath/NestedPayload.scala @@ -1,11 +1,11 @@ package io.oath -import io.circe.generic.semiauto.* -import io.circe.parser.* -import io.circe.syntax.* +import io.circe.generic.semiauto._ +import io.circe.parser._ +import io.circe.syntax._ import io.circe.{Decoder, Encoder} import io.oath.NestedPayload.SimplePayload -import io.oath.json.* +import io.oath.json._ final case class NestedPayload(name: String, mapping: Map[String, SimplePayload]) diff --git a/oath/core/src/test/scala/io/oath/OathManagerSpec.scala b/oath/core/src/test/scala/io/oath/OathManagerSpec.scala index f7c464e..4f0f5e6 100644 --- a/oath/core/src/test/scala/io/oath/OathManagerSpec.scala +++ b/oath/core/src/test/scala/io/oath/OathManagerSpec.scala @@ -1,7 +1,7 @@ package io.oath import io.oath.OathManager.JManager -import io.oath.syntax.all.* +import io.oath.syntax.all._ import io.oath.testkit.WordSpecBase class OathManagerSpec extends WordSpecBase { diff --git a/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala b/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala index 6ece294..b295a47 100644 --- a/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala +++ b/oath/core/src/test/scala/io/oath/OathVerifierSpec.scala @@ -2,7 +2,7 @@ package io.oath import io.oath.OathIssuer.JIssuer import io.oath.OathVerifier.JVerifier -import io.oath.syntax.all.* +import io.oath.syntax.all._ import io.oath.testkit.WordSpecBase class OathVerifierSpec extends WordSpecBase { diff --git a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala index 3aa3237..3ac3ffa 100644 --- a/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/AlgorithmLoaderSpec.scala @@ -2,7 +2,7 @@ package io.oath.config import com.auth0.jwt.JWT import com.typesafe.config.ConfigFactory -import io.oath.testkit.* +import io.oath.testkit._ class AlgorithmLoaderSpec extends WordSpecBase, PropertyBasedTesting { val AlgorithmConfigLocation = "algorithm" diff --git a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala index 6ab6ce4..e3266ec 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtIssuerLoaderSpec.scala @@ -1,7 +1,7 @@ package io.oath.config import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.testkit.* +import io.oath.testkit._ import scala.concurrent.duration.DurationInt @@ -13,6 +13,7 @@ class JwtIssuerLoaderSpec extends WordSpecBase { val WithoutPrivateKeyTokenConfigLocation = "without-private-key-token" val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + val InvalidAudienceClaimsConfigLocation = "invalid-seq-empty-string" "IssuerLoader" should { "load default-token issuer config values from configuration file" in { @@ -73,5 +74,11 @@ class JwtIssuerLoaderSpec extends WordSpecBase { the[ConfigException.BadValue] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) } + + "fail to load issuer config when audience claims contain empty string in configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidAudienceClaimsConfigLocation) + + the[IllegalArgumentException] thrownBy JwtIssuerConfig.loadOrThrow(configLoader) + } } } diff --git a/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala index d7dcbc0..65c485b 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtManagerLoaderSpec.scala @@ -1,7 +1,7 @@ package io.oath.config import com.typesafe.config.ConfigFactory -import io.oath.testkit.* +import io.oath.testkit._ import scala.concurrent.duration.DurationInt diff --git a/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala index 794b3ce..a5d850f 100644 --- a/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala +++ b/oath/core/src/test/scala/io/oath/config/JwtVerifierLoaderSpec.scala @@ -1,7 +1,7 @@ package io.oath.config import com.typesafe.config.{ConfigException, ConfigFactory} -import io.oath.testkit.* +import io.oath.testkit._ import scala.concurrent.duration.DurationInt @@ -13,6 +13,7 @@ class JwtVerifierLoaderSpec extends WordSpecBase { val WithoutPublicKeyTokenConfigLocation = "without-public-key-token" val InvalidTokenEmptyStringConfigLocation = "invalid-token-empty-string" val InvalidTokenWrongTypeConfigLocation = "invalid-token-wrong-type" + val InvalidAudienceClaimsConfigLocation = "invalid-seq-empty-string" "VerifierLoader" should { "load default-token verifier config values from configuration file" in { @@ -74,5 +75,11 @@ class JwtVerifierLoaderSpec extends WordSpecBase { the[ConfigException.WrongType] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) } + + "fail to load verifier config when audience claims contain empty string in configuration file" in { + val configLoader = ConfigFactory.load(configFile).getConfig(InvalidAudienceClaimsConfigLocation) + + the[IllegalArgumentException] thrownBy JwtVerifierConfig.loadOrThrow(configLoader) + } } } diff --git a/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala index 8c22a2e..efe7746 100644 --- a/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala +++ b/oath/core/src/test/scala/io/oath/testkit/Arbitraries.scala @@ -1,13 +1,13 @@ package io.oath.testkit import com.auth0.jwt.algorithms.Algorithm -import io.oath.NestedHeader.* -import io.oath.NestedPayload.* -import io.oath.* +import io.oath.NestedHeader._ +import io.oath.NestedPayload._ +import io.oath._ import io.oath.config.JwtIssuerConfig.RegisteredConfig -import io.oath.config.JwtVerifierConfig.* -import io.oath.config.* -import org.scalacheck.* +import io.oath.config.JwtVerifierConfig._ +import io.oath.config._ +import org.scalacheck._ import java.time.Instant import scala.concurrent.duration.{Duration, DurationInt} diff --git a/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala b/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala index d541282..72473f8 100644 --- a/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala +++ b/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/conversion.scala @@ -1,8 +1,21 @@ package io.oath.jsoniter_scala -import com.github.plokhotnyuk.jsoniter_scala.core.* -import io.oath.json.* -import io.oath.jsoniter_scala.syntax.* +import com.github.plokhotnyuk.jsoniter_scala.core._ +import io.oath.JwtVerifyError +import io.oath.json._ -object conversion: - given [P](using codec: JsonValueCodec[P]): ClaimsCodec[P] = codec.convert +import scala.util.control.Exception.allCatch + +object conversion { + given [P](using codec: JsonValueCodec[P]): ClaimsCodec[P] = new ClaimsCodec[P] { + override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = + allCatch + .withTry(readFromString(token)(codec)) + .toEither + .left + .map(error => JwtVerifyError.DecodingError(error.getMessage, error.getCause)) + + override def encode(data: P): String = + writeToString(data)(codec) + } +} diff --git a/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala b/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala deleted file mode 100644 index a414d52..0000000 --- a/oath/jsoniter-scala/src/main/scala/io/oath/jsoniter_scala/syntax.scala +++ /dev/null @@ -1,20 +0,0 @@ -package io.oath.jsoniter_scala - -import com.github.plokhotnyuk.jsoniter_scala.core.* -import io.oath.JwtVerifyError -import io.oath.json.ClaimsCodec - -import scala.util.control.Exception.allCatch - -object syntax: - extension [P](codec: JsonValueCodec[P]) - def convert: ClaimsCodec[P] = new ClaimsCodec[P]: - override def decode(token: String): Either[JwtVerifyError.DecodingError, P] = - allCatch - .withTry(readFromString(token)(codec)) - .toEither - .left - .map(error => JwtVerifyError.DecodingError(error.getMessage, error.getCause)) - - override def encode(data: P): String = - writeToString(data)(codec) diff --git a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala index a5b71aa..0276458 100644 --- a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala +++ b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/Bar.scala @@ -1,11 +1,10 @@ package io.oath.jsoniter_scala -import com.github.plokhotnyuk.jsoniter_scala.core.* -import com.github.plokhotnyuk.jsoniter_scala.macros.* -import io.oath.json.ClaimsCodec -import io.oath.jsoniter_scala.syntax.* +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ final case class Bar(name: String, age: Int) -object Bar: - implicit val codecBar: ClaimsCodec[Bar] = JsonCodecMaker.make.convert +object Bar { + given JsonValueCodec[Bar] = JsonCodecMaker.make[Bar] +} diff --git a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala index 58c99b7..86fb421 100644 --- a/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala +++ b/oath/jsoniter-scala/src/test/scala/io/oath/jsoniter_scala/JsoniterConversionSpec.scala @@ -2,13 +2,14 @@ package io.oath.jsoniter_scala import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.oath.* +import io.oath._ import io.oath.config.JwtIssuerConfig.RegisteredConfig -import io.oath.config.JwtVerifierConfig.* +import io.oath.config.JwtVerifierConfig._ import io.oath.config.{JwtIssuerConfig, JwtVerifierConfig} import io.oath.json.ClaimsDecoder -import io.oath.syntax.* -import io.oath.syntax.all.* +import io.oath.jsoniter_scala.conversion.given +import io.oath.syntax._ +import io.oath.syntax.all._ import io.oath.testkit.CodecHelper.unsafeParseJsonToJavaMap import io.oath.testkit.WordSpecBase @@ -30,7 +31,6 @@ class JsoniterConversionSpec extends WordSpecBase { val jwtIssuer = JwtIssuer(issuerConfig) "JsoniterConversion" should { - "convert jsoniter codec to claims (encoders & decoders)" in { val bar = Bar("bar", 10) val jwt = jwtIssuer.issueJwt(JwtClaims.ClaimsP(bar)).value diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c985873..a243c4b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,13 +5,13 @@ object Dependencies { private lazy val scalaTestV = "3.2.19" private lazy val scalaTestPlusCheckV = "3.2.18.0" private lazy val scalacheckV = "1.18.1" - private lazy val javaJWTV = "4.4.0" + private lazy val javaJWTV = "4.5.0" private lazy val configV = "1.4.3" - private lazy val bcprovV = "1.79" + private lazy val bcprovV = "1.80" private lazy val circeV = "0.14.10" - private lazy val jsoniterScalaV = "2.31.3" - private lazy val catsV = "2.12.0" - private lazy val tinkV = "1.15.0" + private lazy val jsoniterScalaV = "2.33.1" + private lazy val catsV = "2.13.0" + private lazy val tinkV = "1.16.0" // Testing lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestV diff --git a/project/plugins.sbt b/project/plugins.sbt index a52a6f3..273d9ba 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") -addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") +//addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.7.7")