From 0d1d119480885143325d31bfedb163c9fcd1bd6e Mon Sep 17 00:00:00 2001 From: Tobias Binna Date: Tue, 11 Nov 2025 15:04:45 +0800 Subject: [PATCH] Update to Scala 3 Closes #55 --- README.md | 4 +- build.sbt | 9 +- .../jwt/HttpRequestCanonicalizer.scala | 182 ++++++++-------- .../toolsplus/atlassian/jwt/JwtWriter.scala | 8 +- .../jwt/HttpRequestCanonicalizerSpec.scala | 19 +- .../atlassian/jwt/JwtBuilderSpec.scala | 11 +- .../jwt/JwtClaimSetVerifiersSpec.scala | 61 +++--- .../atlassian/jwt/JwtJsonBuilderSpec.scala | 26 +-- .../atlassian/jwt/JwtParserSpec.scala | 6 +- .../asymmetric/AsymmetricJwtReaderSpec.scala | 194 ++++++++++-------- .../symmetric/SymmetricJwtReaderSpec.scala | 6 +- project/Dependencies.scala | 2 +- project/build.properties | 2 +- 13 files changed, 287 insertions(+), 243 deletions(-) diff --git a/README.md b/README.md index 6e3f3ca..cc4f42a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ [![Continuous integration](https://github.com/toolsplus/atlassian-jwt/actions/workflows/ci.yml/badge.svg)](https://github.com/toolsplus/atlassian-jwt/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/toolsplus/atlassian-jwt/branch/master/graph/badge.svg)](https://codecov.io/gh/toolsplus/atlassian-jwt) -[![Maven Central](https://img.shields.io/maven-central/v/io.toolsplus/atlassian-jwt-core_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.toolsplus/atlassian-jwt-core_2.12) +[![Maven Central](https://img.shields.io/maven-central/v/io.toolsplus/atlassian-jwt-core_3.svg)](https://maven-badges.herokuapp.com/maven-central/io.toolsplus/atlassian-jwt-core_3) Utilities to read, validate and generate valid Atlassian JWTs. Atlassian tokens are identical to regular JWTs with the exception of a few custom claims, such as `qsh` claim. ## Quick start -atlassian-jwt is published to Maven Central Scala 2.13: +atlassian-jwt is published to Maven Central for Scala 3: libraryDependencies += "io.toolsplus" %% "atlassian-jwt" % "x.x.x" diff --git a/build.sbt b/build.sbt index 51c36a3..a11d7d4 100644 --- a/build.sbt +++ b/build.sbt @@ -3,10 +3,13 @@ import xerial.sbt.Sonatype.sonatypeCentralHost val commonSettings = Seq( organization := "io.toolsplus", - scalaVersion := "2.13.16", + scalaVersion := "3.3.6", versionScheme := Some("early-semver"), - resolvers ++= Seq(Resolver.typesafeRepo("releases")) ++ Resolver - .sonatypeOssRepos("releases") + resolvers ++= Seq( + Resolver.typesafeRepo("releases"), + Resolver + .sonatypeCentralRepo("releases") + ) ) lazy val publishSettings = Seq( diff --git a/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizer.scala b/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizer.scala index e2cb5e8..7119d7c 100644 --- a/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizer.scala +++ b/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizer.scala @@ -1,112 +1,113 @@ package io.toolsplus.atlassian.jwt -import java.io.UnsupportedEncodingException import java.net.URLEncoder -import java.security.{MessageDigest, NoSuchAlgorithmException} +import java.security.MessageDigest import io.toolsplus.atlassian.jwt.api.CanonicalHttpRequest import org.bouncycastle.util.encoders.Hex -/** - * Instructions for computing the query hash parameter ("qsh") from a HTTP request. +/** Instructions for computing the query hash parameter ("qsh") from a HTTP + * request. * ------------------------------------------------------------------------------------- * - * Overview: query hash = hash(canonical-request) + * Overview: query hash = hash(canonical-request) * - * canonical-request = canonical-method + '&' + canonical-URI + '&' + canonical-query-string + * canonical-request = canonical-method + '&' + canonical-URI + '&' + + * canonical-query-string * - * 1. Compute canonical method. - * Simply the upper-case of the method name (e.g. "GET", "PUT"). - * - * 2. Append the character '&' - * - * 3. Compute canonical URI. - * Discard the protocol, server, port, context path and query parameters from the full URL. - * For requests targeting add-ons discard the `baseUrl` in the add-on descriptor. - * (Removing the context path allows a reverse proxy to redirect incoming requests for "jira.example.com/getsomething" - * to "example.com/jira/getsomething" without breaking authentication. The requester cannot know that the reverse proxy - * will prepend the context path "/jira" to the originally requested path "/getsomething".) - * Empty-string is not permitted; use "/" instead. - * Do not suffix with a '/' character unless it is the only character. - * Url-encode any '&' characters in the path. - * E.g. in "http://server:80/some/path/?param=value" the canonical URI is "/some/path" - * and in "http://server:80" the canonical URI is "/". - * - * 4. Append the character '&'. - * - * 5. Compute the canonical query string. - * Sort the query parameters primarily by their percent-encoded names and secondarily by their percent-encoded values. - * Sorting is by codepoint: sort(["a", "A", "b", "B"]) => ["A", "B", "a", "b"]. - * For each parameter append its percent-encoded name, the '=' character and then its percent-encoded value. - * In the case of repeated parameters append the ',' character and subsequent percent-encoded values. - * Ignore the JWT query string parameter, if present. - * Some particular values to be aware of: "+" is encoded as "%20", - * "*" as "%2A" and - * "~" as "~". - * (These values used for consistency with OAuth1.) - * An example: for a GET request to the not-yet-percent-encoded URL - * "http://localhost:2990/path/to/service?zee_last=param&repeated=parameter 1&first=param& - * repeated=parameter 2" - * the canonical request is "GET&/path/to/service&first=param&repeated=parameter%201,parameter%202& - * zee_last=param". - * - * 6. Convert the canonical request string to bytes. - * The encoding used to represent characters as bytes is UTF-8. - * - * 7. Hash the canonical request bytes using the SHA-256 algorithm. - * E.g. The SHA-256 hash of "foo" is "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae". + * 1. Compute canonical method. Simply the upper-case of the method name + * (e.g. "GET", "PUT"). + * 2. Append the character '&' + * 3. Compute canonical URI. Discard the protocol, server, port, context path + * and query parameters from the full URL. For requests targeting add-ons + * discard the `baseUrl` in the add-on descriptor. (Removing the context + * path allows a reverse proxy to redirect incoming requests for + * "jira.example.com/getsomething" to "example.com/jira/getsomething" + * without breaking authentication. The requester cannot know that the + * reverse proxy will prepend the context path "/jira" to the originally + * requested path "/getsomething".) Empty-string is not permitted; use "/" + * instead. Do not suffix with a '/' character unless it is the only + * character. Url-encode any '&' characters in the path. E.g. in + * "http://server:80/some/path/?param=value" the canonical URI is + * "/some/path" and in "http://server:80" the canonical URI is "/". + * 4. Append the character '&'. + * 5. Compute the canonical query string. Sort the query parameters primarily + * by their percent-encoded names and secondarily by their percent-encoded + * values. Sorting is by codepoint: sort(["a", "A", "b", "B"]) => ["A", + * "B", "a", "b"]. For each parameter append its percent-encoded name, the + * '=' character and then its percent-encoded value. In the case of + * repeated parameters append the ',' character and subsequent + * percent-encoded values. Ignore the JWT query string parameter, if + * present. Some particular values to be aware of: "+" is encoded as + * "%20", "*" as "%2A" and "~" as "~". (These values used for consistency + * with OAuth1.) An example: for a GET request to the + * not-yet-percent-encoded URL + * "http://localhost:2990/path/to/service?zee_last=param&repeated=parameter + * 1&first=param& repeated=parameter 2" the canonical request is + * "GET&/path/to/service&first=param&repeated=parameter%201,parameter%202& + * zee_last=param". + * 6. Convert the canonical request string to bytes. The encoding used to + * represent characters as bytes is UTF-8. + * 7. Hash the canonical request bytes using the SHA-256 algorithm. E.g. The + * SHA-256 hash of "foo" is + * "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae". */ object HttpRequestCanonicalizer { val QueryStringHashClaimName: String = "qsh" - /** - * When the JWT message is specified in the query string of a URL then this is the parameter name. + /** When the JWT message is specified in the query string of a URL then this + * is the parameter name. * - * E.g. "jwt" in: - *
+    * E.g. "jwt" in: 
     * http://server:80/some/path?otherparam=value&jwt=eyJhbGciOiJIUzI1NiIsI.eyJleHAiOjEzNzg5NCI6MTM3ODk1MjQ4OH0
-    * .cDihfcsKW_We_EY21tIs55dVwjU
-    * 
+ * .cDihfcsKW_We_EY21tIs55dVwjU
*/ private val JwtParamName: String = "jwt" - /** - * Query parameter separator as it appears between "value1" and "param2" in the URL - * "http://server/path?param1=value1&param2=value2". + /** Query parameter separator as it appears between "value1" and "param2" in + * the URL "http://server/path?param1=value1&param2=value2". */ private val QueryParamsSeparator: Char = '&' - /** - * The character between "a" and "b%20c" in "some_param=a,b%20c" + /** The character between "a" and "b%20c" in "some_param=a,b%20c" */ private val EncodedParamValueSeparator: String = "," - /** - * For separating the method, URI etc in a canonical request string. + /** For separating the method, URI etc in a canonical request string. */ private[jwt] val CanonicalRequestPartSeparator: Char = '&' - /** - * Assemble the components of the HTTP request into the correct format so that they can be signed or hashed. + /** Assemble the components of the HTTP request into the correct format so + * that they can be signed or hashed. * - * @param request [[CanonicalHttpRequest]] that provides the necessary components - * @return String encoding the canonical form of this request as required for constructing query string hash values - * @throws UnsupportedEncodingException [[UnsupportedEncodingException]] if the [[java.net.URLEncoder]] cannot encode the request's field's characters + * @param request + * [[CanonicalHttpRequest]] that provides the necessary components + * @return + * String encoding the canonical form of this request as required for + * constructing query string hash values + * @throws UnsupportedEncodingException + * [[java.io.UnsupportedEncodingException]] if the [[java.net.URLEncoder]] + * cannot encode the request's field's characters */ def canonicalize(request: CanonicalHttpRequest): String = s"${canonicalizeMethod(request)}$CanonicalRequestPartSeparator" + s"${canonicalizeUri(request)}$CanonicalRequestPartSeparator" + s"${canonicalizeQueryParameters(request)}" - /** - * Canonicalize the given [[CanonicalHttpRequest]] and hash it. - * This request hash can be included as a JWT claim to verify that request components are genuine. + /** Canonicalize the given [[CanonicalHttpRequest]] and hash it. This request + * hash can be included as a JWT claim to verify that request components are + * genuine. * - * @param request CanonicalHttpRequest to be canonicalized and hashed - * @return String hash suitable for use as a JWT claim value - * @throws UnsupportedEncodingException if the [[java.net.URLEncoder]] cannot encode the request's field's characters - * @throws NoSuchAlgorithmException if the hashing algorithm does not exist at runtime + * @param request + * CanonicalHttpRequest to be canonicalized and hashed + * @return + * String hash suitable for use as a JWT claim value + * @throws UnsupportedEncodingException + * if the [[java.net.URLEncoder]] cannot encode the request's field's + * characters + * @throws NoSuchAlgorithmException + * if the hashing algorithm does not exist at runtime */ def computeCanonicalRequestHash(request: CanonicalHttpRequest): String = { // prevent the code in this method being repeated in every call site that needs a request hash, @@ -114,13 +115,15 @@ object HttpRequestCanonicalizer { computeSha256Hash(canonicalize(request)) } - /** - * Compute the SHA-256 hash of hashInput. - * E.g. The SHA-256 hash of "foo" is "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae". + /** Compute the SHA-256 hash of hashInput. E.g. The SHA-256 hash of "foo" is + * "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae". * - * @param hashInput String to be hashed. - * @return String hash - * @throws NoSuchAlgorithmException if the hashing algorithm does not exist at runtime + * @param hashInput + * String to be hashed. + * @return + * String hash + * @throws NoSuchAlgorithmException + * if the hashing algorithm does not exist at runtime */ private def computeSha256Hash(hashInput: String): String = { val digest = MessageDigest.getInstance("SHA-256") @@ -153,18 +156,17 @@ object HttpRequestCanonicalizer { } private[jwt] def canonicalizeQueryParameters( - request: CanonicalHttpRequest): String = { + request: CanonicalHttpRequest + ): String = { (request.parameterMap - JwtParamName).toSeq - .sortBy { - case (key, values) => - s"${percentEncode(key)} ${percentEncode(values.mkString(","))}" + .sortBy { case (key, values) => + s"${percentEncode(key)} ${percentEncode(values.mkString(","))}" } .map((percentEncodePair _).tupled) .mkString(QueryParamsSeparator.toString) } - /** - * Construct a form-urlencoded document from the given name/parameter pair. + /** Construct a form-urlencoded document from the given name/parameter pair. */ private def percentEncodePair(key: String, values: Seq[String]): String = { val encKey = percentEncode(key) @@ -173,13 +175,15 @@ object HttpRequestCanonicalizer { s"$encKey=$encVal" } - /** - * Encode value using URLEncoder.encode() but encode some characters differently to URLEncoder, to match OAuth1 - * and VisualVault. + /** Encode value using URLEncoder.encode() but encode some characters + * differently to URLEncoder, to match OAuth1 and VisualVault. * - * @param value String to be percent-encoded - * @return encoded Encoded result string - * @throws UnsupportedEncodingException if URLEncoder does not support UTF-8 + * @param value + * String to be percent-encoded + * @return + * encoded Encoded result string + * @throws UnsupportedEncodingException + * if URLEncoder does not support UTF-8 */ private def percentEncode(value: String): String = URLEncoder diff --git a/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/JwtWriter.scala b/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/JwtWriter.scala index eb5a6f9..a370a6a 100644 --- a/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/JwtWriter.scala +++ b/modules/core/src/main/scala/io/toolsplus/atlassian/jwt/JwtWriter.scala @@ -1,14 +1,12 @@ package io.toolsplus.atlassian.jwt -import cats.syntax.either._ import com.nimbusds.jose._ import scala.util.{Failure, Success, Try} -/** - * JWT Writer to write valid Atlassian compatible JWTs. +/** JWT Writer to write valid Atlassian compatible JWTs. * - * Each writer has to be configured with the [[JWSAlgorithm]] and [[JWSSigner]] + * Each writer has to be configured with the [[JWSAlgorithm]] and [[JWSSigner]] * that will be used sign the token. */ case class JwtWriter(algorithm: JWSAlgorithm, signer: JWSSigner) { @@ -23,7 +21,7 @@ case class JwtWriter(algorithm: JWSAlgorithm, signer: JWSSigner) { val jwsObject = new JWSObject(header, new Payload(payload)) Try(jwsObject.sign(signer)) match { - case Success(_) => Right(jwsObject) + case Success(_) => Right(jwsObject) case Failure(exception) => Left(JwtSigningError(exception.getMessage, exception)) } diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizerSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizerSpec.scala index 4b43a09..14af154 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizerSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/HttpRequestCanonicalizerSpec.scala @@ -3,17 +3,15 @@ package io.toolsplus.atlassian.jwt import io.toolsplus.atlassian.jwt.generators.core.JwtGen import io.toolsplus.atlassian.jwt.generators.nimbus.NimbusGen -class HttpRequestCanonicalizerSpec - extends TestSpec - with JwtGen - with NimbusGen { +class HttpRequestCanonicalizerSpec extends TestSpec with JwtGen with NimbusGen { "Using a HttpRequestCanonicalizer" when { "given a valid CanonicalHttpRequest" should { "compute the correct canonical request string" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => def partsOf(r: String) = r.split(HttpRequestCanonicalizer.CanonicalRequestPartSeparator) val canonicalizedRequest = @@ -23,14 +21,16 @@ class HttpRequestCanonicalizerSpec } "compute the correct canonical method string" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val canonicalizedMethod = HttpRequestCanonicalizer.canonicalizeMethod(request) canonicalizedMethod mustBe canonicalizedMethod.toUpperCase } "compute the correct canonical uri string" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val canonicalizedUri = HttpRequestCanonicalizer.canonicalizeUri(request) canonicalizedUri must startWith("/") @@ -39,10 +39,11 @@ class HttpRequestCanonicalizerSpec } "successfully compute canonical request hash" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val canonicalRequestHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) - + canonicalRequestHash must not be empty } } diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtBuilderSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtBuilderSpec.scala index e3b2eac..7658ca6 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtBuilderSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtBuilderSpec.scala @@ -8,7 +8,7 @@ import io.toolsplus.atlassian.jwt.generators.util.JwtTestHelper import org.scalacheck.Gen._ import org.scalatest.Assertion -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ class JwtBuilderSpec extends TestSpec { @@ -48,8 +48,8 @@ class JwtBuilderSpec extends TestSpec { "successfully create JWT claims with overridden issue time" in { val expireAfter = Duration.of(10, ChronoUnit.SECONDS) - val expectedIssuedAt = Instant.now plus Duration.of(5, - ChronoUnit.SECONDS) + val expectedIssuedAt = + Instant.now plus Duration.of(5, ChronoUnit.SECONDS) val result = new JwtBuilder(expireAfter) .withIssuedAt(expectedIssuedAt.getEpochSecond) .build(JwtTestHelper.defaultSigningSecret) @@ -126,8 +126,9 @@ class JwtBuilderSpec extends TestSpec { } - private def validate(assertion: Jwt => Assertion)( - result: Either[JwtSigningError, RawJwt]) = { + private def validate( + assertion: Jwt => Assertion + )(result: Either[JwtSigningError, RawJwt]) = { result match { case Right(rawJwt) => JwtParser.parse(rawJwt) match { diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtClaimSetVerifiersSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtClaimSetVerifiersSpec.scala index 7312810..055088d 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtClaimSetVerifiersSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtClaimSetVerifiersSpec.scala @@ -13,15 +13,16 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { "hasIssueTimeAndExpirationTime" should { "succeed if claims have issue time (iat) and expiry (exp)" in forAll( - jwtClaimsSetGen()) { claims => + jwtClaimsSetGen() + ) { claims => JwtClaimSetVerifiers .hasIssueTimeAndExpirationTime(claims) - .right .value mustBe a[JWTClaimsSet] } "fail if issue time (iat) is missing" in forAll( - jwtClaimsSetGen(Seq("iat" -> null))) { claims => + jwtClaimsSetGen(Seq("iat" -> null)) + ) { claims => JwtClaimSetVerifiers .hasIssueTimeAndExpirationTime(claims) .left @@ -29,7 +30,8 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { } "fail if issue time (exp) is missing" in forAll( - jwtClaimsSetGen(Seq("exp" -> null))) { claims => + jwtClaimsSetGen(Seq("exp" -> null)) + ) { claims => JwtClaimSetVerifiers .hasIssueTimeAndExpirationTime(claims) .left @@ -44,23 +46,24 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { val expDate = Date.from(now.plusMinutes(5).toInstant) "succeed if expiry (exp) is after not before (nbf) time" in forAll( - jwtClaimsSetGen(Seq("nbf" -> nowDate, "exp" -> expDate))) { claims => + jwtClaimsSetGen(Seq("nbf" -> nowDate, "exp" -> expDate)) + ) { claims => JwtClaimSetVerifiers .expirationTimeIsAfterNotBefore(claims) - .right .value mustBe a[JWTClaimsSet] } "succeed if not before (nbf) is missing" in forAll( - jwtClaimsSetGen(Seq("nbf" -> null))) { claims => + jwtClaimsSetGen(Seq("nbf" -> null)) + ) { claims => JwtClaimSetVerifiers .expirationTimeIsAfterNotBefore(claims) - .right .value mustBe a[JWTClaimsSet] } "fail if expiry (exp) is missing" in forAll( - jwtClaimsSetGen(Seq("exp" -> null, "nbf" -> nowDate))) { claims => + jwtClaimsSetGen(Seq("exp" -> null, "nbf" -> nowDate)) + ) { claims => JwtClaimSetVerifiers .expirationTimeIsAfterNotBefore(claims) .left @@ -68,7 +71,8 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { } "fail if expiry (exp) is earlier than not before (nbf) time" in forAll( - jwtClaimsSetGen(Seq("exp" -> nowDate, "nbf" -> expDate))) { claims => + jwtClaimsSetGen(Seq("exp" -> nowDate, "nbf" -> expDate)) + ) { claims => JwtClaimSetVerifiers .expirationTimeIsAfterNotBefore(claims) .left @@ -86,17 +90,20 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { JwtClaimSetVerifiers.nowIsAfterNotBefore(now.toInstant, 30) "succeed if current time is after not before (nbf) time" in forAll( - jwtClaimsSetGen(Seq("nbf" -> fiveMinutesAgo))) { claims => - validator(claims).right.value mustBe a[JWTClaimsSet] + jwtClaimsSetGen(Seq("nbf" -> fiveMinutesAgo)) + ) { claims => + validator(claims).value mustBe a[JWTClaimsSet] } "succeed if not before (nbf) is missing" in forAll( - jwtClaimsSetGen(Seq("nbf" -> null))) { claims => - validator(claims).right.value mustBe a[JWTClaimsSet] + jwtClaimsSetGen(Seq("nbf" -> null)) + ) { claims => + validator(claims).value mustBe a[JWTClaimsSet] } "fail if expiry (exp) is earlier than not before (nbf) time" in forAll( - jwtClaimsSetGen(Seq("nbf" -> inFiveMinutes))) { claims => + jwtClaimsSetGen(Seq("nbf" -> inFiveMinutes)) + ) { claims => validator(claims).left.value mustBe a[JwtTooEarlyError] } } @@ -111,17 +118,20 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { JwtClaimSetVerifiers.nowIsBeforeExpirationTime(now.toInstant, 30) "succeed if current time is before expiry (exp) time" in forAll( - jwtClaimsSetGen(Seq("exp" -> inFiveMinutes))) { claims => - validator(claims).right.value mustBe a[JWTClaimsSet] + jwtClaimsSetGen(Seq("exp" -> inFiveMinutes)) + ) { claims => + validator(claims).value mustBe a[JWTClaimsSet] } "fail if expiry (exp) time is missing" in forAll( - jwtClaimsSetGen(Seq("exp" -> null))) { claims => + jwtClaimsSetGen(Seq("exp" -> null)) + ) { claims => validator(claims).left.value mustBe a[JwtInvalidClaimError] } "fail if current time is after expiry (exp) time" in forAll( - jwtClaimsSetGen(Seq("exp" -> fiveMinutesAgo))) { claims => + jwtClaimsSetGen(Seq("exp" -> fiveMinutesAgo)) + ) { claims => validator(claims).left.value mustBe a[JwtExpiredError] } } @@ -134,17 +144,20 @@ class JwtClaimSetVerifiersSpec extends TestSpec with EitherValues { JwtClaimSetVerifiers.queryStringHash(qsh) "succeed if qsh claim matches expected qsh" in forAll( - jwtClaimsSetGen(Seq("qsh" -> qsh))) { claims => - validator(claims).right.value mustBe a[JWTClaimsSet] + jwtClaimsSetGen(Seq("qsh" -> qsh)) + ) { claims => + validator(claims).value mustBe a[JWTClaimsSet] } "succeed if qsh claim is missing" in forAll( - jwtClaimsSetGen(Seq("qsh" -> null))) { claims => - validator(claims).right.value mustBe a[JWTClaimsSet] + jwtClaimsSetGen(Seq("qsh" -> null)) + ) { claims => + validator(claims).value mustBe a[JWTClaimsSet] } "fail if qsh claim does not match expected qsh" in forAll( - jwtClaimsSetGen(Seq("qsh" -> "non-matching-qsh"))) { claims => + jwtClaimsSetGen(Seq("qsh" -> "non-matching-qsh")) + ) { claims => validator(claims).left.value mustBe a[JwtInvalidClaimError] } diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtJsonBuilderSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtJsonBuilderSpec.scala index 592bd28..d266d47 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtJsonBuilderSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtJsonBuilderSpec.scala @@ -38,10 +38,10 @@ class JwtJsonBuilderSpec extends TestSpec { val expectedExpiry = Instant.now plusSeconds defaultLifetime issuedAt .getOption(json) - .getOrElse(fail) mustBe now.getEpochSecond +- toleranceSeconds + .getOrElse(fail()) mustBe now.getEpochSecond +- toleranceSeconds expiry .getOption(json) - .getOrElse(fail) mustBe expectedExpiry.getEpochSecond +- leewaySeconds + .getOrElse(fail()) mustBe expectedExpiry.getEpochSecond +- leewaySeconds } validate(assertion)(result) @@ -55,7 +55,7 @@ class JwtJsonBuilderSpec extends TestSpec { val expectedExpiry = Instant.now plus expireAfter expiry .getOption(json) - .getOrElse(fail) mustBe expectedExpiry.getEpochSecond +- toleranceSeconds + .getOrElse(fail()) mustBe expectedExpiry.getEpochSecond +- toleranceSeconds } validate(assertion)(result) @@ -72,7 +72,7 @@ class JwtJsonBuilderSpec extends TestSpec { def assertion(json: Json) = { expiry .getOption(json) - .getOrElse(fail) mustBe expectedExpiry.getEpochSecond +- toleranceSeconds + .getOrElse(fail()) mustBe expectedExpiry.getEpochSecond +- toleranceSeconds } validate(assertion)(result) @@ -88,7 +88,7 @@ class JwtJsonBuilderSpec extends TestSpec { def assertion(json: Json) = { issuedAt .getOption(json) - .getOrElse(fail) mustBe expectedIssueTime +- toleranceSeconds + .getOrElse(fail()) mustBe expectedIssueTime +- toleranceSeconds } validate(assertion)(result) @@ -99,7 +99,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withAudience(expectedAudience).build def assertion(json: Json) = { - audience.getOption(json).getOrElse(fail) mustBe expectedAudience + audience.getOption(json).getOrElse(fail()) mustBe expectedAudience } validate(assertion)(result) @@ -111,7 +111,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withIssuer(expectedIssuer).build def assertion(json: Json) = { - issuer.getOption(json).getOrElse(fail) mustBe expectedIssuer + issuer.getOption(json).getOrElse(fail()) mustBe expectedIssuer } validate(assertion)(result) @@ -123,7 +123,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withJwtId(expectedJwtId).build def assertion(json: Json) = { - jwtId.getOption(json).getOrElse(fail) mustBe expectedJwtId + jwtId.getOption(json).getOrElse(fail()) mustBe expectedJwtId } validate(assertion)(result) @@ -141,7 +141,7 @@ class JwtJsonBuilderSpec extends TestSpec { def assertion(json: Json) = { notBefore .getOption(json) - .getOrElse(fail) mustBe expectedNotBefore.getEpochSecond +- toleranceSeconds + .getOrElse(fail()) mustBe expectedNotBefore.getEpochSecond +- toleranceSeconds } validate(assertion)(result) @@ -152,7 +152,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withSubject(expectedSubject).build def assertion(json: Json) = { - subject.getOption(json).getOrElse(fail) mustBe expectedSubject + subject.getOption(json).getOrElse(fail()) mustBe expectedSubject } validate(assertion)(result) @@ -164,7 +164,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withType(expectedType).build def assertion(json: Json) = { - `type`.getOption(json).getOrElse(fail) mustBe expectedType + `type`.getOption(json).getOrElse(fail()) mustBe expectedType } validate(assertion)(result) @@ -176,7 +176,7 @@ class JwtJsonBuilderSpec extends TestSpec { val result = JwtJsonBuilder().withQueryHash(expectedQsh).build def assertion(json: Json) = { - queryStringHash.getOption(json).getOrElse(fail) mustBe expectedQsh + queryStringHash.getOption(json).getOrElse(fail()) mustBe expectedQsh } validate(assertion)(result) @@ -190,7 +190,7 @@ class JwtJsonBuilderSpec extends TestSpec { def assertion(json: Json) = { customClaim(claimName) .getOption(json) - .getOrElse(fail) mustBe claimValue + .getOrElse(fail()) mustBe claimValue } validate(assertion)(result) diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtParserSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtParserSpec.scala index 1fad82b..2b9166c 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtParserSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/JwtParserSpec.scala @@ -16,7 +16,7 @@ class JwtParserSpec extends TestSpec { "successfully parse a JWT" in forAll(signedSymmetricJwtStringGen()) { token => JwtParser.parse(token) match { case Right(jwt) => jwt mustBe a[Jwt] - case Left(_) => fail + case Left(_) => fail() } } @@ -26,7 +26,7 @@ class JwtParserSpec extends TestSpec { case Right(jwsObject) => jwsObject mustBe a[JWSObject] jwsObject.serialize() mustBe token - case Left(_) => fail + case Left(_) => fail() } } @@ -36,7 +36,7 @@ class JwtParserSpec extends TestSpec { case Right(parsedClaims) => parsedClaims mustBe a[JWTClaimsSet] JSONObjectUtils.toJSONString(parsedClaims.toJSONObject) mustBe JSONObjectUtils.toJSONString(claims.toJSONObject) - case Left(_) => fail + case Left(_) => fail() } } diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/asymmetric/AsymmetricJwtReaderSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/asymmetric/AsymmetricJwtReaderSpec.scala index c896d44..d206d16 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/asymmetric/AsymmetricJwtReaderSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/asymmetric/AsymmetricJwtReaderSpec.scala @@ -4,7 +4,7 @@ import com.nimbusds.jose.JWSAlgorithm import io.toolsplus.atlassian.jwt._ import io.toolsplus.atlassian.jwt.api.Predef.RawJwt import io.toolsplus.atlassian.jwt.generators.util.RSAKeyPairGenerator -import org.scalacheck.{Gen, Shrink} +import org.scalacheck.Gen import java.security.interfaces.RSAPublicKey import java.time.{Duration, Instant} @@ -21,9 +21,11 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { private val jwtReader = AsymmetricJwtReader(publicKey, appBaseUrl) private def jwtGen(qsh: String): Gen[RawJwt] = - signedAsymmetricJwtStringGen(keyId, - privateKey, - Seq("qsh" -> qsh, "aud" -> appBaseUrl)) + signedAsymmetricJwtStringGen( + keyId, + privateKey, + Seq("qsh" -> qsh, "aud" -> appBaseUrl) + ) "Using a AsymmetricJwtReader" when { @@ -43,13 +45,17 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { } "successfully read and verify a JWT even if qsh is not present (self-authenticated)" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) forAll( - signedAsymmetricJwtStringGen(keyId, - privateKey, - Seq("aud" -> appBaseUrl))) { token => + signedAsymmetricJwtStringGen( + keyId, + privateKey, + Seq("aud" -> appBaseUrl) + ) + ) { token => jwtReader .readAndVerify(token, queryHash) match { case Right(jwt) => jwt mustBe a[Jwt] @@ -62,18 +68,21 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { forAll(canonicalHttpRequestGen) { request => val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) forAll( - signedAsymmetricJwtStringGen(keyId, - privateKey, - Seq.empty, - JWSAlgorithm.RS512)) { token => + signedAsymmetricJwtStringGen( + keyId, + privateKey, + Seq.empty, + JWSAlgorithm.RS512 + ) + ) { token => jwtReader .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtInvalidSigningAlgorithmError] + case Left(e) => e mustBe a[JwtInvalidSigningAlgorithmError] case Right(jwt) => fail( - s"Expected validation for JWT ($jwt) with 'alg' claim to fail") + s"Expected validation for JWT ($jwt) with 'alg' claim to fail" + ) } } } @@ -84,19 +93,22 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val notBefore = Instant.now plus Duration.ofMinutes(45) - val customClaims = Seq("qsh" -> queryHash, - "aud" -> appBaseUrl, - "nbf" -> Date.from(notBefore)) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) - forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { - token => - jwtReader - .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtInvalidClaimError] - case Right(jwt) => - fail( - s"Expected validation for JWT ($jwt) with 'nbf' claim to fail") - } + val customClaims = Seq( + "qsh" -> queryHash, + "aud" -> appBaseUrl, + "nbf" -> Date.from(notBefore) + ) + forAll( + signedAsymmetricJwtStringGen(keyId, privateKey, customClaims) + ) { token => + jwtReader + .readAndVerify(token, queryHash) match { + case Left(e) => e mustBe a[JwtInvalidClaimError] + case Right(jwt) => + fail( + s"Expected validation for JWT ($jwt) with 'nbf' claim to fail" + ) + } } } } @@ -106,19 +118,22 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val notBefore = Instant.now plus Duration.ofMinutes(3) - val customClaims = Seq("qsh" -> queryHash, - "aud" -> appBaseUrl, - "nbf" -> Date.from(notBefore)) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) - forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { - token => - jwtReader - .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtTooEarlyError] - case Right(jwt) => - fail( - s"Expected validation for JWT ($jwt) with 'nbf' claim to fail") - } + val customClaims = Seq( + "qsh" -> queryHash, + "aud" -> appBaseUrl, + "nbf" -> Date.from(notBefore) + ) + forAll( + signedAsymmetricJwtStringGen(keyId, privateKey, customClaims) + ) { token => + jwtReader + .readAndVerify(token, queryHash) match { + case Left(e) => e mustBe a[JwtTooEarlyError] + case Right(jwt) => + fail( + s"Expected validation for JWT ($jwt) with 'nbf' claim to fail" + ) + } } } } @@ -128,18 +143,20 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val expiry = Instant.now minus Duration.ofMinutes(3) - val customClaims = Seq("qsh" -> queryHash, - "aud" -> appBaseUrl, - "exp" -> Date.from(expiry)) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) - forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { - token => - jwtReader - .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtExpiredError] - case Right(jwt) => - fail(s"Expected validation for expired JWT ($jwt) to fail") - } + val customClaims = Seq( + "qsh" -> queryHash, + "aud" -> appBaseUrl, + "exp" -> Date.from(expiry) + ) + forAll( + signedAsymmetricJwtStringGen(keyId, privateKey, customClaims) + ) { token => + jwtReader + .readAndVerify(token, queryHash) match { + case Left(e) => e mustBe a[JwtExpiredError] + case Right(jwt) => + fail(s"Expected validation for expired JWT ($jwt) to fail") + } } } } @@ -150,16 +167,17 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val customClaims = Seq("qsh" -> queryHash, "aud" -> appBaseUrl, "iat" -> null) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) - forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { - token => - jwtReader - .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtInvalidClaimError] - case Right(jwt) => - fail( - s"Expected validation for JWT ($jwt) without issue time to fail") - } + forAll( + signedAsymmetricJwtStringGen(keyId, privateKey, customClaims) + ) { token => + jwtReader + .readAndVerify(token, queryHash) match { + case Left(e) => e mustBe a[JwtInvalidClaimError] + case Right(jwt) => + fail( + s"Expected validation for JWT ($jwt) without issue time to fail" + ) + } } } } @@ -170,16 +188,17 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val customClaims = Seq("qsh" -> queryHash, "aud" -> appBaseUrl, "exp" -> null) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) - forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { - token => - jwtReader - .readAndVerify(token, queryHash) match { - case Left(e) => e mustBe a[JwtInvalidClaimError] - case Right(jwt) => - fail( - s"Expected validation for JWT ($jwt) without expiration time to fail") - } + forAll( + signedAsymmetricJwtStringGen(keyId, privateKey, customClaims) + ) { token => + jwtReader + .readAndVerify(token, queryHash) match { + case Left(e) => e mustBe a[JwtInvalidClaimError] + case Right(jwt) => + fail( + s"Expected validation for JWT ($jwt) without expiration time to fail" + ) + } } } } @@ -189,47 +208,52 @@ class AsymmetricJwtReaderSpec extends TestSpec with RSAKeyPairGenerator { "given an invalid JWT string" should { "fail to read a JWT with query string hash mismatch" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) - forAll(signedAsymmetricJwtStringGen(keyId, - privateKey, - Seq("qsh" -> queryHash, - "aud" -> appBaseUrl)), - Gen.alphaNumStr) { (token, invalidQsh) => + forAll( + signedAsymmetricJwtStringGen( + keyId, + privateKey, + Seq("qsh" -> queryHash, "aud" -> appBaseUrl) + ), + Gen.alphaNumStr + ) { (token, invalidQsh) => jwtReader .readAndVerify(token, invalidQsh) match { case Left(failure) => failure mustBe a[JwtVerificationError] - case Right(_) => fail + case Right(_) => fail() } } } "fail to read a JWT from an unsigned token string" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) forAll(unsignedJwtStringGen(Seq("qsh" -> queryHash))) { token => jwtReader .readAndVerify(token, queryHash) match { case Left(failure) => failure mustBe a[ParsingFailure] - case Right(_) => fail + case Right(_) => fail() } } } "fail to read a JWT from a token string with invalid signature" in forAll( - canonicalHttpRequestGen) { request => + canonicalHttpRequestGen + ) { request => val queryHash = HttpRequestCanonicalizer.computeCanonicalRequestHash(request) val customClaims = Seq("qsh" -> queryHash, "aud" -> appBaseUrl) - implicit val rawJwtNoShrink = Shrink[RawJwt](_ => Stream.empty) forAll(signedAsymmetricJwtStringGen(keyId, privateKey, customClaims)) { token => jwtReader .readAndVerify(token.dropRight(5), queryHash) match { case Left(failure) => failure mustBe a[JwtSignatureMismatchError] - case Right(_) => fail + case Right(_) => fail() } } } diff --git a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/symmetric/SymmetricJwtReaderSpec.scala b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/symmetric/SymmetricJwtReaderSpec.scala index 894bfc0..13e931a 100644 --- a/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/symmetric/SymmetricJwtReaderSpec.scala +++ b/modules/core/src/test/scala/io/toolsplus/atlassian/jwt/symmetric/SymmetricJwtReaderSpec.scala @@ -177,7 +177,7 @@ class SymmetricJwtReaderSpec extends TestSpec { SymmetricJwtReader(signingSecret) .readAndVerify(token, invalidQsh) match { case Left(failure) => failure mustBe a[JwtVerificationError] - case Right(_) => fail + case Right(_) => fail() } } } @@ -190,7 +190,7 @@ class SymmetricJwtReaderSpec extends TestSpec { SymmetricJwtReader(signingSecret) .readAndVerify(token, queryHash) match { case Left(failure) => failure mustBe a[ParsingFailure] - case Right(_) => fail + case Right(_) => fail() } } } @@ -206,7 +206,7 @@ class SymmetricJwtReaderSpec extends TestSpec { SymmetricJwtReader(signingSecret) .readAndVerify(token.dropRight(5), queryHash) match { case Left(failure) => failure mustBe a[JwtSignatureMismatchError] - case Right(_) => fail + case Right(_) => fail() } } } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 8789f9b..f313a02 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -27,7 +27,7 @@ object Version { val bouncyCastle = "1.81" val circe = "0.14.14" val scalaTest = "3.2.19" - val scalaCheck = "1.18.1" + val scalaCheck = "1.19.0" val scalaTestPlusScalaCheck = "3.2.18.0" val scalaCheckDateTime = "0.7.0" } diff --git a/project/build.properties b/project/build.properties index be54e77..a360cca 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.10.0 +sbt.version = 1.11.7