diff --git a/README.md b/README.md index 6e3f3ca..cc4f42a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ [](https://github.com/toolsplus/atlassian-jwt/actions/workflows/ci.yml) [](https://codecov.io/gh/toolsplus/atlassian-jwt) -[](https://maven-badges.herokuapp.com/maven-central/io.toolsplus/atlassian-jwt-core_2.12) +[](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¶m2=value2".
+ /** Query parameter separator as it appears between "value1" and "param2" in
+ * the URL "http://server/path?param1=value1¶m2=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