From 714e3de2a009a3b79b1c3a914fa935456afcb832 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 22:41:04 +0100 Subject: [PATCH 01/43] remove scalastyle from project. --- build.sbt | 16 ------- project/plugins.sbt | 2 - scalastyle-config.xml | 98 -------------------------------------- scripts/pre-commit-hook.sh | 66 ------------------------- 4 files changed, 182 deletions(-) delete mode 100644 scalastyle-config.xml delete mode 100644 scripts/pre-commit-hook.sh diff --git a/build.sbt b/build.sbt index c5ca7cb..f410f04 100644 --- a/build.sbt +++ b/build.sbt @@ -52,19 +52,3 @@ val dependencies = Seq( libraryDependencies ++= dependencies libraryDependencies ++= testDependencies - -lazy val scalastyleTest = taskKey[Unit]("scalastyleTest") -scalastyleTest := (scalastyle in Test).toTask("").value - -(scalastyle in Compile) := ((scalastyle in Compile) dependsOn scalastyleTest).toTask("").value - -lazy val installGitHook = taskKey[Unit]("Installs git hooks") -installGitHook := { - if (sys.props("os.name").contains("Windows")) { - "cmd /c copy scripts\\pre-commit-hook.sh .git\\hooks\\pre-commit" ! - } else { - "cp scripts/pre-commit-hook.sh .git/hooks/pre-commit" ! - } -} - -(compile in Compile) := ((compile in Compile) dependsOn installGitHook).value diff --git a/project/plugins.sbt b/project/plugins.sbt index b7728a3..4c1476e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1 @@ -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") - addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") diff --git a/scalastyle-config.xml b/scalastyle-config.xml deleted file mode 100644 index 66d8a97..0000000 --- a/scalastyle-config.xml +++ /dev/null @@ -1,98 +0,0 @@ - - Scalastyle standard configuration - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/scripts/pre-commit-hook.sh b/scripts/pre-commit-hook.sh deleted file mode 100644 index e03693b..0000000 --- a/scripts/pre-commit-hook.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -echo "" -echo "Doing some Checks..." -echo "* Stashing non-staged changes so we don't check them" -git diff --quiet -hadNoNonStagedChanges=$? - -if ! [ $hadNoNonStagedChanges -eq 0 ] -then - git stash --keep-index -u > /dev/null -fi - -echo "* Compiling..." -sbt test:compile > /dev/null -compiles=$? - -if [ $compiles -ne 0 ] -then - echo " [KO] Error compiling " -else - echo "* Testing..." - sbt test > /dev/null - test=$? - if [ $test -ne 0 ] - then - echo " [KO] Test failures " - else - echo "* Checking code style…" - - sbt scalastyle > /dev/null - productionScalastyle=$? - - if [ $productionScalastyle -ne 0 ] - then - echo " [KO] Error checking code style" - fi - fi -fi - -echo "* Applying the stash with the non-staged changes…" -if ! [ $hadNoNonStagedChanges -eq 0 ] -then - sleep 1 && git stash pop --index > /dev/null & # sleep because otherwise commit fails when this leads to a merge conflict -fi - -# Final result -echo "" - -if [ $compiles -eq 0 ] && [ $productionScalastyle -eq 0 ] && [ $test -eq 0 ] -then - echo "[OK] Your code will be committed young padawan" - exit 0 -elif [ $compiles -ne 0 ] -then - echo "[KO] Cancelling commit due to compile error (run 'sbt test:compile' for more information)" - exit 1 -elif [ $test -ne 0 ] -then - echo "[KO] Cancelling commit due to test failures (run 'sbt test' for more information)" - exit 1 -elif [ $productionScalastyle -ne 0 ] -then - echo "[KO] Cancelling commit due to code style error (run 'sbt scalastyle' for more information)" - exit 2 -fi From 90af8bb48f82d8ddba7362880bb2bc29dd147b14 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 22:41:52 +0100 Subject: [PATCH 02/43] update README. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 725036b..6d8ba10 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,3 @@ val jsValueOpt = Jwt.decode(token, "super_fancy_secret") - RS256 - RS384 - RS512 - -## Source Code Style -Check style via [scalastyle](http://www.scalastyle.org/): - -```sbtshell -sbt scalastyle -``` From 478875b5bec5ca1390cd56c7b14a1904b6579239 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 22:44:58 +0100 Subject: [PATCH 03/43] remove misplaced import in build.sbt --- build.sbt | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.sbt b/build.sbt index f410f04..21b998b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,3 @@ -import scala.sys.process._ - name := "spray-jwt" organization := "com.github.janjaali" From e5fbe2e07998ef3394f82af88bf3c44cb04d4115 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 22:53:02 +0100 Subject: [PATCH 04/43] initialize multi module project structure. --- build.sbt | 87 ++++++++----------- .../scala/org/janjaali/sprayjwt/Jwt.scala | 0 .../org/janjaali/sprayjwt/JwtClaims.scala | 0 .../janjaali/sprayjwt/algorithms/HS256.scala | 0 .../janjaali/sprayjwt/algorithms/HS384.scala | 0 .../janjaali/sprayjwt/algorithms/HS512.scala | 0 .../algorithms/HashingAlgorithm.scala | 0 .../sprayjwt/algorithms/HmacAlgorithm.scala | 0 .../janjaali/sprayjwt/algorithms/RS256.scala | 0 .../janjaali/sprayjwt/algorithms/RS384.scala | 0 .../janjaali/sprayjwt/algorithms/RS512.scala | 0 .../sprayjwt/algorithms/RsaAlgorithm.scala | 0 .../sprayjwt/encoder/Base64Decoder.scala | 0 .../sprayjwt/encoder/Base64Encoder.scala | 0 .../sprayjwt/encoder/ByteEncoder.scala | 0 .../InvalidJwtAlgorithmException.scala | 0 .../exceptions/InvalidJwtException.scala | 0 .../InvalidJwtHeaderException.scala | 0 .../InvalidSignatureException.scala | 0 .../janjaali/sprayjwt/headers/JwtHeader.scala | 0 .../janjaali/sprayjwt/headers/headers.scala | 0 .../janjaali/sprayjwt/test.rsa.private.key | 0 .../org/janjaali/sprayjwt/test.rsa.public.key | 0 .../scala/org/janjaali/sprayjwt/JwtSpec.scala | 0 .../algorithms/HashingAlgorithmSpec.scala | 0 .../sprayjwt/encoder/Base64DecoderSpec.scala | 0 .../sprayjwt/encoder/Base64EncoderSpec.scala | 0 .../sprayjwt/encoder/ByteEncoderSpec.scala | 0 .../headers/JwtHeaderJsonProtocolSpec.scala | 0 29 files changed, 37 insertions(+), 50 deletions(-) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/Jwt.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/JwtClaims.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtAlgorithmException.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtException.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtHeaderException.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/exceptions/InvalidSignatureException.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala (100%) rename {src => spray-jwt/src}/main/scala/org/janjaali/sprayjwt/headers/headers.scala (100%) rename {src => spray-jwt/src}/test/resources/org/janjaali/sprayjwt/test.rsa.private.key (100%) rename {src => spray-jwt/src}/test/resources/org/janjaali/sprayjwt/test.rsa.public.key (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/JwtSpec.scala (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala (100%) rename {src => spray-jwt/src}/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala (100%) diff --git a/build.sbt b/build.sbt index 21b998b..2c87306 100644 --- a/build.sbt +++ b/build.sbt @@ -1,52 +1,39 @@ -name := "spray-jwt" - -organization := "com.github.janjaali" - -version := "1.0.0" - -licenses := Seq("MIT License" -> url("https://opensource.org/licenses/MIT")) - -homepage := Some(url("https://github.com/janjaali/spray-jwt")) - -scmInfo := Some( - ScmInfo( - url("https://github.com/janjaali/spray-jwt"), - "scm:git@github.com/janjaali/spray-jwt.git" - ) -) - -developers := List( - Developer( - id = "ghashange", - name = "ghashange", - email = "", - url = url("https://github.com/janjaali") - ) -) - scalaVersion := "2.12.3" -publishMavenStyle := true - -publishArtifact in Test := false - -publishTo := { - val nexus = "https://oss.sonatype.org/" - if (isSnapshot.value) { - Some("snapshots" at nexus + "content/repositories/snapshots") - } else { - Some("releases" at nexus + "service/local/staging/deploy/maven2") - } -} - -val testDependencies = Seq( - "org.scalatest" %% "scalatest" % "3.0.4" % "test" -) - -val dependencies = Seq( - "io.spray" %% "spray-json" % "1.3.3", - "org.bouncycastle" % "bcpkix-jdk15on" % "1.58" -) - -libraryDependencies ++= dependencies -libraryDependencies ++= testDependencies +lazy val sprayJwt = (project in file("spray-jwt")) + .settings( + name := "spray-jwt", + organization := "com.github.janjaali", + version := "1.0.0-SNAPSHOT", + licenses := Seq("MIT License" -> url("https://opensource.org/licenses/MIT")), + homepage := Some(url("https://github.com/janjaali/spray-jwt")), + scmInfo := Some( + ScmInfo( + url("https://github.com/janjaali/spray-jwt"), + "scm:git@github.com/janjaali/spray-jwt.git" + ) + ), + developers := List( + Developer( + id = "ghashange", + name = "ghashange", + email = "", + url = url("https://github.com/janjaali") + ) + ), + publishMavenStyle := true, + publishArtifact in Test := false, + publishTo := { + val nexus = "https://oss.sonatype.org/" + if (isSnapshot.value) { + Some("snapshots" at nexus + "content/repositories/snapshots") + } else { + Some("releases" at nexus + "service/local/staging/deploy/maven2") + } + }, + libraryDependencies ++= Seq( + "io.spray" %% "spray-json" % "1.3.3", + "org.bouncycastle" % "bcpkix-jdk15on" % "1.58", + "org.scalatest" %% "scalatest" % "3.0.4" % Test + ) + ) diff --git a/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/Jwt.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtAlgorithmException.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtAlgorithmException.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtAlgorithmException.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtAlgorithmException.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtException.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtException.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtException.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtException.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtHeaderException.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtHeaderException.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtHeaderException.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidJwtHeaderException.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidSignatureException.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidSignatureException.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidSignatureException.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/exceptions/InvalidSignatureException.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala diff --git a/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala similarity index 100% rename from src/main/scala/org/janjaali/sprayjwt/headers/headers.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala diff --git a/src/test/resources/org/janjaali/sprayjwt/test.rsa.private.key b/spray-jwt/src/test/resources/org/janjaali/sprayjwt/test.rsa.private.key similarity index 100% rename from src/test/resources/org/janjaali/sprayjwt/test.rsa.private.key rename to spray-jwt/src/test/resources/org/janjaali/sprayjwt/test.rsa.private.key diff --git a/src/test/resources/org/janjaali/sprayjwt/test.rsa.public.key b/spray-jwt/src/test/resources/org/janjaali/sprayjwt/test.rsa.public.key similarity index 100% rename from src/test/resources/org/janjaali/sprayjwt/test.rsa.public.key rename to spray-jwt/src/test/resources/org/janjaali/sprayjwt/test.rsa.public.key diff --git a/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala diff --git a/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala diff --git a/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala diff --git a/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala diff --git a/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala diff --git a/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala similarity index 100% rename from src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala From 43775f1752664dea2f7fd0b426c2acb4250cef31 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 23:03:25 +0100 Subject: [PATCH 05/43] initialize spray-jwt-akka-http-test project. --- build.sbt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/build.sbt b/build.sbt index 2c87306..0538f8f 100644 --- a/build.sbt +++ b/build.sbt @@ -37,3 +37,13 @@ lazy val sprayJwt = (project in file("spray-jwt")) "org.scalatest" %% "scalatest" % "3.0.4" % Test ) ) + +lazy val sprayJwtAkkaHttpTest = (project in file("spray-jwt-akka-http-test")) + .dependsOn(sprayJwt) + .settings( + name := "spray-jwt-akka-http-test", + libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-http" % "10.0.11", + "com.typesafe.akka" %% "akka-http-testkit" % "10.0.11" % Test + ) + ) From f3eda616878eed872bd64ee1d8c110139698bddd Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 26 Feb 2018 23:23:29 +0100 Subject: [PATCH 06/43] initial draft with first route for spray-jwt-akka-http-test. --- build.sbt | 5 ++- .../src/main/resources/application.conf | 3 ++ .../janjaali/sprayjwt/akkahttp/Server.scala | 35 +++++++++++++++++++ .../sprayjwt/akkahttp/ServerRoutes.scala | 19 ++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 spray-jwt-akka-http-test/src/main/resources/application.conf create mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala create mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala diff --git a/build.sbt b/build.sbt index 0538f8f..082f478 100644 --- a/build.sbt +++ b/build.sbt @@ -44,6 +44,9 @@ lazy val sprayJwtAkkaHttpTest = (project in file("spray-jwt-akka-http-test")) name := "spray-jwt-akka-http-test", libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % "10.0.11", - "com.typesafe.akka" %% "akka-http-testkit" % "10.0.11" % Test + "com.typesafe.akka" %% "akka-http-testkit" % "10.0.11" % Test, + + "com.typesafe.scala-logging" %% "scala-logging" % "3.8.0", + "ch.qos.logback" % "logback-classic" % "1.2.3" ) ) diff --git a/spray-jwt-akka-http-test/src/main/resources/application.conf b/spray-jwt-akka-http-test/src/main/resources/application.conf new file mode 100644 index 0000000..2cc2e31 --- /dev/null +++ b/spray-jwt-akka-http-test/src/main/resources/application.conf @@ -0,0 +1,3 @@ +server { + port = 8080 +} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala new file mode 100644 index 0000000..7995003 --- /dev/null +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala @@ -0,0 +1,35 @@ +package org.janjaali.sprayjwt.akkahttp + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.stream.ActorMaterializer +import com.typesafe.config.ConfigFactory +import com.typesafe.scalalogging.LazyLogging + +import scala.concurrent.ExecutionContextExecutor +import scala.util.{Failure, Success} + +object Server extends LazyLogging { + def main(args: Array[String]): Unit = { + logger.info("Starting spray-jwt-akka-http Server") + + implicit val system: ActorSystem = ActorSystem("spray-jwt-akka-http-test") + implicit val executionContext: ExecutionContextExecutor = system.dispatcher + implicit val materializer: ActorMaterializer = ActorMaterializer.create(system) + + val config = ConfigFactory.defaultApplication() + + val host = "0.0.0.0" + val port = config.getInt("server.port") + + val routes = new ServerRoutes().routes + + Http().bindAndHandle(routes, host, port) + .onComplete { + case Success(_) => logger.info(s"Server starter on $host:$port") + case Failure(ex) => + logger.error(s"Server failed to start due to ${ex.getMessage}", ex) + System.exit(1) + } + } +} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala new file mode 100644 index 0000000..b5a7dbb --- /dev/null +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala @@ -0,0 +1,19 @@ +package org.janjaali.sprayjwt.akkahttp + +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.{Directives, ExceptionHandler, Route} +import com.typesafe.scalalogging.LazyLogging + +class ServerRoutes extends Directives with LazyLogging { + def routes: Route = { + handleExceptions(defaultExceptionHandler) { + complete("dance") + } + } + + val defaultExceptionHandler = ExceptionHandler { + case ex: Exception => + logger.error(s"Catch unhandled exception in defaultExceptionHandler", ex) + complete(StatusCodes.InternalServerError, "ups") + } +} From b697107d580db06e7aa62c14a54765ba25cfdf05 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Thu, 1 Mar 2018 20:48:45 +0100 Subject: [PATCH 07/43] Rename ServerRoutes to ApiRoutes. --- .../sprayjwt/akkahttp/{ServerRoutes.scala => ApiRoutes.scala} | 2 +- .../src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/{ServerRoutes.scala => ApiRoutes.scala} (90%) diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala similarity index 90% rename from spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala rename to spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala index b5a7dbb..ae9b0ab 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ServerRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala @@ -4,7 +4,7 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.{Directives, ExceptionHandler, Route} import com.typesafe.scalalogging.LazyLogging -class ServerRoutes extends Directives with LazyLogging { +class ApiRoutes extends Directives with LazyLogging { def routes: Route = { handleExceptions(defaultExceptionHandler) { complete("dance") diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala index 7995003..73f273e 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala @@ -22,7 +22,7 @@ object Server extends LazyLogging { val host = "0.0.0.0" val port = config.getInt("server.port") - val routes = new ServerRoutes().routes + val routes = new ApiRoutes().routes Http().bindAndHandle(routes, host, port) .onComplete { From e17f36be43a09a1bcd77936900eb96f6286a9c3d Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Thu, 1 Mar 2018 21:15:35 +0100 Subject: [PATCH 08/43] initialize tokenRoute --- .../org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala | 10 ++++++---- .../org/janjaali/sprayjwt/akkahttp/Server.scala | 7 ++++++- .../sprayjwt/akkahttp/token/TokenRoutes.scala | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala index ae9b0ab..682fa18 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala @@ -1,13 +1,15 @@ package org.janjaali.sprayjwt.akkahttp import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.{Directives, ExceptionHandler, Route} +import akka.http.scaladsl.server.{Directives, ExceptionHandler, Route, RouteConcatenation} import com.typesafe.scalalogging.LazyLogging -class ApiRoutes extends Directives with LazyLogging { - def routes: Route = { +class ApiRoutes(routes: Route*) extends Directives with LazyLogging { + def route: Route = { handleExceptions(defaultExceptionHandler) { - complete("dance") + pathPrefix("api" / "rest") { + RouteConcatenation.concat(routes:_*) + } } } diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala index 73f273e..835c660 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala @@ -5,6 +5,7 @@ import akka.http.scaladsl.Http import akka.stream.ActorMaterializer import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging +import org.janjaali.sprayjwt.akkahttp.token.TokenRoutes import scala.concurrent.ExecutionContextExecutor import scala.util.{Failure, Success} @@ -22,7 +23,11 @@ object Server extends LazyLogging { val host = "0.0.0.0" val port = config.getInt("server.port") - val routes = new ApiRoutes().routes + val tokenRoutes = new TokenRoutes + + val routes = new ApiRoutes( + tokenRoutes.routes + ).route Http().bindAndHandle(routes, host, port) .onComplete { diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala new file mode 100644 index 0000000..47aee27 --- /dev/null +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala @@ -0,0 +1,13 @@ +package org.janjaali.sprayjwt.akkahttp.token + +import akka.http.scaladsl.server.{Directives, Route} + +class TokenRoutes extends Directives { + val routes: Route = Route { + path("token") { + get { + complete("dance") + } + } + } +} From 5359e3e787d0a402810104dd39e7fbcc493e7915 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Thu, 1 Mar 2018 21:21:33 +0100 Subject: [PATCH 09/43] impl. getting jwt token on GET /token call. --- .../sprayjwt/akkahttp/token/TokenRoutes.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala index 47aee27..7ecf2c9 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala @@ -1,12 +1,19 @@ package org.janjaali.sprayjwt.akkahttp.token import akka.http.scaladsl.server.{Directives, Route} +import org.janjaali.sprayjwt.Jwt +import org.janjaali.sprayjwt.algorithms.HS256 +import spray.json.{JsObject, JsString} class TokenRoutes extends Directives { val routes: Route = Route { path("token") { - get { - complete("dance") + rejectEmptyResponse { + get { + val payload = JsObject("dance" -> JsString("in the rain")) + val jwt = Jwt.encode(payload, "secret", HS256) + complete(jwt) + } } } } From b76abe69626399cbc2a123c07fa4b54efbb8ad87 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Thu, 1 Mar 2018 21:56:27 +0100 Subject: [PATCH 10/43] add route for secured resource. --- build.sbt | 1 + .../src/main/resources/application.conf | 8 ++++++++ .../janjaali/sprayjwt/akkahttp/Server.scala | 10 ++++++++-- .../resource/SecuredResourceRoute.scala | 20 +++++++++++++++++++ .../sprayjwt/akkahttp/token/TokenRoutes.scala | 4 ++-- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala diff --git a/build.sbt b/build.sbt index 082f478..b0d5125 100644 --- a/build.sbt +++ b/build.sbt @@ -45,6 +45,7 @@ lazy val sprayJwtAkkaHttpTest = (project in file("spray-jwt-akka-http-test")) libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % "10.0.11", "com.typesafe.akka" %% "akka-http-testkit" % "10.0.11" % Test, + "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.11", "com.typesafe.scala-logging" %% "scala-logging" % "3.8.0", "ch.qos.logback" % "logback-classic" % "1.2.3" diff --git a/spray-jwt-akka-http-test/src/main/resources/application.conf b/spray-jwt-akka-http-test/src/main/resources/application.conf index 2cc2e31..0300ed1 100644 --- a/spray-jwt-akka-http-test/src/main/resources/application.conf +++ b/spray-jwt-akka-http-test/src/main/resources/application.conf @@ -1,3 +1,11 @@ server { port = 8080 } + +api { + rest { + routes { + secret = "secret" + } + } +} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala index 835c660..c449fa9 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala @@ -5,6 +5,7 @@ import akka.http.scaladsl.Http import akka.stream.ActorMaterializer import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging +import org.janjaali.sprayjwt.akkahttp.resource.SecuredResourceRoute import org.janjaali.sprayjwt.akkahttp.token.TokenRoutes import scala.concurrent.ExecutionContextExecutor @@ -23,10 +24,15 @@ object Server extends LazyLogging { val host = "0.0.0.0" val port = config.getInt("server.port") - val tokenRoutes = new TokenRoutes + val secret = config.getString("api.rest.routes.secret") + + val tokenRoutes = new TokenRoutes(secret) + + val securedResourceRoute = new SecuredResourceRoute(secret) val routes = new ApiRoutes( - tokenRoutes.routes + tokenRoutes.routes, + securedResourceRoute.routes ).route Http().bindAndHandle(routes, host, port) diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala new file mode 100644 index 0000000..895cf9e --- /dev/null +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala @@ -0,0 +1,20 @@ +package org.janjaali.sprayjwt.akkahttp.resource + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.server.{Directives, Route} +import org.janjaali.sprayjwt.Jwt + +class SecuredResourceRoute(secret: String) extends Directives { + val routes: Route = Route { + pathPrefix("secured" / "resource") { + get { + rejectEmptyResponse { + parameter("token") { token => + val payload = Jwt.decode(token, secret) + complete(payload) + } + } + } + } + } +} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala index 7ecf2c9..1748184 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala @@ -5,13 +5,13 @@ import org.janjaali.sprayjwt.Jwt import org.janjaali.sprayjwt.algorithms.HS256 import spray.json.{JsObject, JsString} -class TokenRoutes extends Directives { +class TokenRoutes(secret: String) extends Directives { val routes: Route = Route { path("token") { rejectEmptyResponse { get { val payload = JsObject("dance" -> JsString("in the rain")) - val jwt = Jwt.encode(payload, "secret", HS256) + val jwt = Jwt.encode(payload, secret, HS256) complete(jwt) } } From 9d7a07d85a49487e9b743055d4fd0077d42809e6 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sat, 2 Mar 2019 14:02:08 +0100 Subject: [PATCH 11/43] Update scala-version to 2.12.8. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b0d5125..7c63be1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.3" +scalaVersion := "2.12.8" lazy val sprayJwt = (project in file("spray-jwt")) .settings( From 80527e18f4a98bccea3eb4343e6f1dce95a74668 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sat, 2 Mar 2019 14:02:28 +0100 Subject: [PATCH 12/43] Update sbt-version to 1.2.8. --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 017bb86..dca663d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.0.2 +sbt.version = 1.2.8 From bed077d1c29a2946489a3c895b6ecef8e0c73cea Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:33:33 +0100 Subject: [PATCH 13/43] Bump scala language version to v2.13.4. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7c63be1..915989e 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.8" +scalaVersion := "2.13.4" lazy val sprayJwt = (project in file("spray-jwt")) .settings( From b23138eb685e6609b86f7108f67beb85e83e60b0 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:33:59 +0100 Subject: [PATCH 14/43] Bump sbt version to v1.4.5. --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index dca663d..ed145bb 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.2.8 +sbt.version = 1.4.5 From 8674d3e21e46bc61838e4d3f2117b07c369206a2 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:34:15 +0100 Subject: [PATCH 15/43] Add VS Code and Metals files to gitignore. --- .gitignore | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 92f86aa..3e27ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,16 @@ -.idea - +# Build target project/target + +# IntelliJ +.idea + +# VS Code +.vscode + +# Metals +.bloop +.bsp +.metals +metals.sbt +project/project From 70e75f0cb782d9c2da9e190d7762f00a7173938a Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:39:06 +0100 Subject: [PATCH 16/43] Add version scheme "early-semver" for build. --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 915989e..31d41bd 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,7 @@ scalaVersion := "2.13.4" +ThisBuild / versionScheme := Some("early-semver") + lazy val sprayJwt = (project in file("spray-jwt")) .settings( name := "spray-jwt", From df09a71b810093aa38b406ffdf63941cea0b9873 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:42:48 +0100 Subject: [PATCH 17/43] Fix lib version number in build. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 31d41bd..849fba3 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ lazy val sprayJwt = (project in file("spray-jwt")) .settings( name := "spray-jwt", organization := "com.github.janjaali", - version := "1.0.0-SNAPSHOT", + version := "1.0.0", licenses := Seq("MIT License" -> url("https://opensource.org/licenses/MIT")), homepage := Some(url("https://github.com/janjaali/spray-jwt")), scmInfo := Some( From 16b4c200f2b9af48796154fbbd6f46214e5a768d Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 21 Feb 2021 20:46:55 +0100 Subject: [PATCH 18/43] Update changelog format. --- CHANGELOG.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf41fc..ff2d96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,15 @@ # Changelog -## Version 1.0.0 (2017-11-12) +All notable changes to this project will be documented in this file. -### New features -- Encode and decode JWT token -- suppot hashing algorithms: -- - HS256 -- - HS384 -- - HS512 -- - RS256 -- - RS384 -- - RS512 -- - ES256 -- - ES384 -- - ES512 +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] + +### ✨ Feature + +* Add CHANGELOG to root directory + +## [1.0.0] + +* Encode and decode JWT tokens support for following hashing algorithms (HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384 and ES512) From d50c8d79e1367c09e4a63ef7e4f377fdd65fc18f Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 23 Feb 2021 18:30:06 +0100 Subject: [PATCH 19/43] Added some blank lines to README. --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6d8ba10..851254d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # spray-jwt + JWT library to use with spray-json and akka-http. ## Install + Add spray-jwt as dependency to your `build.sbt`: ```sbtshell @@ -11,6 +13,7 @@ libraryDependencies ++= Seq( ``` To encode a JsValue to a JWT token: + ```scala import org.janjaali.sprayjwt.Jwt import org.janjaali.sprayjwt.algorithms.HS256 @@ -20,6 +23,7 @@ val jwtOpt = Jwt.encode(payload, "super_fancy_secret", HS256) ``` And vice versa to decode JWT token as a JsValue: + ```scala import org.janjaali.sprayjwt.Jwt import org.janjaali.sprayjwt.algorithms.HS256 @@ -29,10 +33,11 @@ val jsValueOpt = Jwt.decode(token, "super_fancy_secret") ``` ## Supported algorithms -- HS256 -- HS384 -- HS512 -- RS256 -- RS384 -- RS512 +* HS256 +* HS384 +* HS512 + +* RS256 +* RS384 +* RS512 From b1906920a6b46d7405b3048ea400ad276ab4f489 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 23 Feb 2021 18:32:49 +0100 Subject: [PATCH 20/43] Fix code block language in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 851254d..2fff878 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ JWT library to use with spray-json and akka-http. Add spray-jwt as dependency to your `build.sbt`: -```sbtshell +```sbt libraryDependencies ++= Seq( "com.github.janjaali" %% "spray-jwt" % "1.0.0" ) From a2e0c8d3c4ca01ec77b7d3271f92d0b2c0f1f71a Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Sun, 14 Mar 2021 17:17:13 +0100 Subject: [PATCH 21/43] Add scalafmt and scalafix. --- .scalafix.conf | 17 +++++++++++++++++ .scalafmt.conf | 1 + 2 files changed, 18 insertions(+) create mode 100644 .scalafix.conf create mode 100644 .scalafmt.conf diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..57bf7db --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,17 @@ +// https://github.com/liancheng/scalafix-organize-imports +OrganizeImports { + blankLines = Auto + coalesceToWildcardImportThreshold = 5 + expandRelative = false + groupExplicitlyImportedImplicitsSeparately = false + groupedImports = Merge + groups = [ + "*" + "java." + "scala." + ] + importSelectorsOrder = Ascii + importsOrder = Ascii + preset = DEFAULT + removeUnused = true +} diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..ffbdff9 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +version = "2.7.4" From 55cdd77e1149f8f5c8f6ca42469f3158cc6b895e Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Mon, 15 Mar 2021 21:38:44 +0100 Subject: [PATCH 22/43] WIP --- build.sbt | 11 +- .../resource/SecuredResourceRoute.scala | 4 +- .../sprayjwt/akkahttp/token/TokenRoutes.scala | 4 +- .../scala/org/janjaali/sprayjwt/Jwt.scala | 187 +++++++++++++++--- .../org/janjaali/sprayjwt/JwtClaims.scala | 1 + .../sprayjwt/algorithms/Algorithm.scala | 58 ++++++ .../janjaali/sprayjwt/algorithms/HS256.scala | 10 - .../janjaali/sprayjwt/algorithms/HS384.scala | 10 - .../janjaali/sprayjwt/algorithms/HS512.scala | 10 - .../algorithms/HashingAlgorithm.scala | 42 ---- .../sprayjwt/algorithms/HmacAlgorithm.scala | 49 +++-- .../janjaali/sprayjwt/algorithms/RS256.scala | 10 - .../janjaali/sprayjwt/algorithms/RS384.scala | 10 - .../janjaali/sprayjwt/algorithms/RS512.scala | 10 - .../sprayjwt/algorithms/RsaAlgorithm.scala | 54 +++-- .../sprayjwt/encoder/Base64Encoder.scala | 25 ++- .../janjaali/sprayjwt/headers/JwtHeader.scala | 4 +- .../janjaali/sprayjwt/headers/headers.scala | 7 +- .../sprayjwt/json/CommonJsonWriters.scala | 90 +++++++++ .../sprayjwt/json/JsonStringSerializer.scala | 9 + .../janjaali/sprayjwt/json/JsonValue.scala | 9 + .../janjaali/sprayjwt/json/JsonWriter.scala | 13 ++ .../janjaali/sprayjwt/jwt/model/Claim.scala | 70 +++++++ .../sprayjwt/jwt/model/ClaimsSet.scala | 34 ++++ .../sprayjwt/jwt/model/JwsPayload.scala | 22 +++ .../sprayjwt/jwt/model/NumericDate.scala | 25 +++ .../scala/org/janjaali/sprayjwt/JwtSpec.scala | 167 ++++++++++------ .../algorithms/HashingAlgorithmSpec.scala | 10 +- .../sprayjwt/encoder/Base64DecoderSpec.scala | 4 +- .../sprayjwt/encoder/Base64EncoderSpec.scala | 4 +- .../sprayjwt/encoder/ByteEncoderSpec.scala | 4 +- .../headers/JwtHeaderJsonProtocolSpec.scala | 4 +- .../sprayjwt/json/CommonJsonWritersSpec.scala | 48 +++++ .../sprayjwt/jwt/model/ClaimSpec.scala | 18 ++ .../sprayjwt/jwt/model/ClaimsSetSpec.scala | 47 +++++ .../sprayjwt/jwt/model/JwsPayloadSpec.scala | 26 +++ .../sprayjwt/jwt/model/NumericDateSpec.scala | 25 +++ .../jwt/model/ScalaCheckGenerators.scala | 65 ++++++ .../sprayjwt/tests/ScalaTestSpec.scala | 6 + 39 files changed, 959 insertions(+), 247 deletions(-) create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonWriter.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/json/CommonJsonWritersSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala diff --git a/build.sbt b/build.sbt index 849fba3..93866f1 100644 --- a/build.sbt +++ b/build.sbt @@ -34,9 +34,18 @@ lazy val sprayJwt = (project in file("spray-jwt")) } }, libraryDependencies ++= Seq( + // JSON "io.spray" %% "spray-json" % "1.3.3", + + // Encryption "org.bouncycastle" % "bcpkix-jdk15on" % "1.58", - "org.scalatest" %% "scalatest" % "3.0.4" % Test + + // Test + "org.scalatest" %% "scalatest" % "3.2.6" % Test, + + // Property based tests + "org.scalacheck" %% "scalacheck" % "1.15.3" % Test, + "org.scalatestplus" %% "scalacheck-1-15" % "3.2.6.0" % Test ) ) diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala index 895cf9e..875d663 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala @@ -2,7 +2,7 @@ package org.janjaali.sprayjwt.akkahttp.resource import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.server.{Directives, Route} -import org.janjaali.sprayjwt.Jwt +import org.janjaali.sprayjwt.LegacyJwt class SecuredResourceRoute(secret: String) extends Directives { val routes: Route = Route { @@ -10,7 +10,7 @@ class SecuredResourceRoute(secret: String) extends Directives { get { rejectEmptyResponse { parameter("token") { token => - val payload = Jwt.decode(token, secret) + val payload = LegacyJwt.decode(token, secret) complete(payload) } } diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala index 1748184..355051d 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala @@ -1,7 +1,7 @@ package org.janjaali.sprayjwt.akkahttp.token import akka.http.scaladsl.server.{Directives, Route} -import org.janjaali.sprayjwt.Jwt +import org.janjaali.sprayjwt.LegacyJwt import org.janjaali.sprayjwt.algorithms.HS256 import spray.json.{JsObject, JsString} @@ -11,7 +11,7 @@ class TokenRoutes(secret: String) extends Directives { rejectEmptyResponse { get { val payload = JsObject("dance" -> JsString("in the rain")) - val jwt = Jwt.encode(payload, secret, HS256) + val jwt = LegacyJwt.encode(payload, secret, HS256) complete(jwt) } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala index 8555b1f..083e903 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala @@ -3,35 +3,143 @@ package org.janjaali.sprayjwt import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.janjaali.sprayjwt.algorithms.HashingAlgorithm +import org.janjaali.sprayjwt.algorithms.Algorithm import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder} -import org.janjaali.sprayjwt.exceptions.{InvalidJwtException, InvalidSignatureException} +import org.janjaali.sprayjwt.exceptions.{ + InvalidJwtException, + InvalidSignatureException +} import org.janjaali.sprayjwt.headers.{JwtHeader, JwtHeaderJsonWriter} import spray.json._ import scala.util.{Success, Try} -/** - * JWT Encoder/Decoder. +import org.janjaali.sprayjwt.json._ +import org.janjaali.sprayjwt.jwt.model.ClaimsSet +import org.janjaali.sprayjwt.jwt.model.JwsPayload + +/** TODO: + */ +trait Claim { + + type T + + def key: String + + def value: T + + def asJson: JsonValue +} + +sealed trait Secret + +/** Represents a JWT consisting of the encoded parts: + * + *
    + *
  1. JOSE Header
  2. + *
  3. JWS Payload
  4. + *
  5. JWS Signature
  6. + *
+ * + * @param jose + * @param claims + * @param signature */ +sealed abstract case class Jwt private ( + joseHeader: String, + jwsPayload: String, + jwsSignature: String +) { + + /** Raw encoded string representation of this JWT. + * + * @return raw JWT string + */ + def raw: String = { + s"$joseHeader.$jwsPayload.$jwsSignature" + } +} + +/** Represents a JOSE Header containing the parameters describing the + * cryptographic operations and parameters employed to encode a JWT value. + * + * // TODO: Is this the JWS Protected Header? (JWS Header) + * + * @param algorithm TODO: define + */ +private case class JoseHeader(algorithm: Algorithm) { + + private val typ = "JWT" + + /** JSON representation of this JOSE Header. + * + * @return JSON object + */ + def asJson: JsonObject = { + JsonObject( + Map( + "typ" -> JsonString(this.typ), + "alg" -> JsonString(algorithm.name) + ) + ) + } +} + object Jwt { + def apply( + claims: ClaimsSet, + algorithm: Algorithm, + secret: String + )(implicit jsonStringSerializer: JsonStringSerializer): Jwt = { + + Jwt( + joseHeader = JoseHeader(algorithm), + jwsPayload = JwsPayload(claims) + ) + } + + private def apply( + joseHeader: JoseHeader, + jwsPayload: JwsPayload + )(implicit jsonStringSerializer: JsonStringSerializer): Jwt = { + + val joseHeaderJson = joseHeader.asJson + val joseHeaderJsonBase64Encoded = Base64Encoder.encode(joseHeaderJson) + + val jwsPayloadJson = jwsPayload.asJson + val jwsPayloadJsonBase64Encoded = Base64Encoder.encode(jwsPayloadJson) + + new Jwt( + joseHeader = joseHeaderJsonBase64Encoded, + jwsPayload = jwsPayloadJsonBase64Encoded, + jwsSignature = ??? + ) {} + } +} + +/** JWT Encoder/Decoder. + */ +object LegacyJwt { + addBouncyCastleProvider() - /** - * Encodes payload as JWT. + /** Encodes payload as JWT. * * @param payload the payload to encode as JWT's payload * @param secret the secret which is used to sign JWT * @param algorithm the hashing algorithm used for encoding * @return encoded JWT */ - def encode(payload: String, secret: String, algorithm: HashingAlgorithm): Try[String] = { + def encode( + payload: String, + secret: String, + algorithm: Algorithm + ): Try[String] = { Try(encode(payload.parseJson, secret, algorithm, None)) } - /** - * Encodes payload as JWT. + /** Encodes payload as JWT. * * @param payload the payload to encode as JWT's payload * @param secret the secret which is used to sign JWT @@ -40,24 +148,31 @@ object Jwt { * payload) * @return encoded JWT */ - def encode(payload: String, secret: String, algorithm: HashingAlgorithm, jwtClaims: JwtClaims): Try[String] = { + def encode( + payload: String, + secret: String, + algorithm: Algorithm, + jwtClaims: JwtClaims + ): Try[String] = { Try(encode(payload.parseJson, secret, algorithm, Some(jwtClaims))) } - /** - * Encodes payload as JWT. + /** Encodes payload as JWT. * * @param payload the payload to encode as JWT's payload * @param secret the secret which is used to sign JWT * @param algorithm the hashing algorithm used for encoding * @return encoded JWT */ - def encode(payload: JsValue, secret: String, algorithm: HashingAlgorithm): Try[String] = { + def encode( + payload: JsValue, + secret: String, + algorithm: Algorithm + ): Try[String] = { Try(encode(payload, secret, algorithm, None)) } - /** - * Encodes payload as JWT. + /** Encodes payload as JWT. * * @param payload the payload to encode as JWT's payload * @param secret the secret which is used to sign JWT @@ -66,12 +181,16 @@ object Jwt { * payload) * @return encoded JWT */ - def encode(payload: JsValue, secret: String, algorithm: HashingAlgorithm, jwtClaims: JwtClaims): Try[String] = { + def encode( + payload: JsValue, + secret: String, + algorithm: Algorithm, + jwtClaims: JwtClaims + ): Try[String] = { Try(encode(payload, secret, algorithm, Some(jwtClaims))) } - /** - * Decodes JWT token as JsValue. + /** Decodes JWT token as JsValue. * * @param token the JWT token to decode * @param secret the secret to use to validate signature of JWT @@ -81,8 +200,7 @@ object Jwt { decodeAsString(token, secret).map(_.parseJson) } - /** - * Decodes JWT token as JsValue. + /** Decodes JWT token as JsValue. * * @param token the JWT token to decode * @param secret the secret to use to validate signature of JWT @@ -91,7 +209,9 @@ object Jwt { def decodeAsString(token: String, secret: String): Try[String] = { val splitToken = token.split("\\.") if (splitToken.length != 3) { - throw new InvalidJwtException("JWT must have form header.payload.signature") + throw new InvalidJwtException( + "JWT must have form header.payload.signature" + ) } val header = splitToken(0) @@ -110,20 +230,26 @@ object Jwt { Success(payloadDecoded) } - private def getAlgorithmFromHeader(header: String): HashingAlgorithm = { + private def getAlgorithmFromHeader(header: String): Algorithm = { val headerDecoded = Base64Decoder.decodeAsString(header) val jwtHeader = headerDecoded.parseJson.convertTo[JwtHeader] jwtHeader.algorithm } - private def encode(payload: JsValue, secret: String, algorithm: HashingAlgorithm, jwtClaims: Option[JwtClaims]): String = { + private def encode( + payload: JsValue, + secret: String, + algorithm: Algorithm, + jwtClaims: Option[JwtClaims] + ): String = { val fields = payload.asJsObject.fields val reversedClaims = jwtClaims.map(getReversedClaims).getOrElse(Map.empty) val payloadWithReservedClaims = JsObject(fields ++ reversedClaims) val encodedHeader = getEncodedHeader(algorithm) - val encodedPayload = Base64Encoder.encode(payloadWithReservedClaims.toString) + val encodedPayload = + Base64Encoder.encode(payloadWithReservedClaims.toString) val encodedData = s"$encodedHeader.$encodedPayload" @@ -137,7 +263,7 @@ object Jwt { } } - private def getEncodedHeader(algorithm: HashingAlgorithm): String = { + private def getEncodedHeader(algorithm: Algorithm): String = { val header = JwtHeader(algorithm).toJson.toString Base64Encoder.encode(header) } @@ -156,15 +282,20 @@ object Jwt { .map(entry => entry._1 -> entry._2.get) .map { case (name, value: String) => name -> JsString(value) - case (name, value: Long) => name -> JsNumber(value) + case (name, value: Long) => name -> JsNumber(value) case (name, values: Set[_]) => if (values.size == 1) { name -> JsString(values.head.asInstanceOf[String]) } else { - name -> JsArray(values.map(v => JsString(v.asInstanceOf[String])).toVector) + name -> JsArray( + values.map(v => JsString(v.asInstanceOf[String])).toVector + ) } - case (name, _) => throw new SerializationException(s"Cannot serialize reserved claim: $name") + case (name, _) => + throw new SerializationException( + s"Cannot serialize reserved claim: $name" + ) } .toMap } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala index f1a46e6..d9440e1 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala @@ -1,5 +1,6 @@ package org.janjaali.sprayjwt +@deprecated("Use jwt.model.Claim.scala") case class JwtClaims( iss: Option[String] = None, sub: Option[String] = None, diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala new file mode 100644 index 0000000..2f09e70 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -0,0 +1,58 @@ +package org.janjaali.sprayjwt.algorithms + +/** Represents a cryptographic algorithms and identifiers used with JWT. + */ +private[sprayjwt] trait Algorithm { + + /** Name of this algorithm. + * + * @return name + */ + def name: String + + // TODO: Maybe a better suited signature + // Digitally signs the contents of the given JWS Header and the JWS Payload. + // def sign(header: JoseHeader, payload: JwsPayload): String + + /** Signs data. // TODO: legacy? + * + * @param data the data to sign + * @param secret the secret to use for signing the data + * @return signed data + */ + def sign(data: String, secret: String): String + + /** Validates signature. // TODO: legacy? + * + * @param signature the signature to validate + * @param data the data to validate signature for + * @param secret the secret to use for validation + * @return true if signature is valid, otherwise returns false + */ + def validate(signature: String, data: String, secret: String): Boolean +} + +/** Companion object to map Strings as hashing algorithms. + */ +object Algorithm { + + private val all = { + List( + HmacAlgorithm.Hs256, + HmacAlgorithm.Hs384, + HmacAlgorithm.Hs512, + RsaAlgorithm.Rs256, + RsaAlgorithm.Rs384, + RsaAlgorithm.Rs512 + ) + } + + /** Resolves a given algorithm name to an algorithm implementation. + * + * @param name name of algorithm + * @return algorithm + */ + def forName(name: String): Option[Algorithm] = { + Algorithm.all.find(_.name == name) + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala deleted file mode 100644 index 911a727..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS256.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents HS256 hashing algorithm. - */ -case object HS256 extends HmacAlgorithm("HS256") { - - override val cryptoAlgName = "HMACSHA256" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala deleted file mode 100644 index 687307e..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS384.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents HS384 hashing algorithm. - */ -case object HS384 extends HmacAlgorithm("HS384") { - - override val cryptoAlgName = "HMACSHA384" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala deleted file mode 100644 index c6d1ac0..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HS512.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents HS512 hashing algorithm. - */ -case object HS512 extends HmacAlgorithm("HS512") { - - override val cryptoAlgName = "HMACSHA512" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala deleted file mode 100644 index 8a3fa8f..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithm.scala +++ /dev/null @@ -1,42 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Companion object to map Strings as hashing algorithms. - */ -object HashingAlgorithm { - def apply(name: String): Option[HashingAlgorithm] = name match { - case "HS256" => Some(HS256) - case "HS384" => Some(HS384) - case "HS512" => Some(HS512) - case "RS256" => Some(RS256) - case "RS384" => Some(RS384) - case "RS512" => Some(RS512) - case _ => None - } -} - -/** - * Represents hashing algorithms used for JWT's. - * - * @param name the name of the hashing algorithm - */ -private[sprayjwt] abstract class HashingAlgorithm(val name: String) { - /** - * Signs data. - * - * @param data the data to sign - * @param secret the secret to use for signing the data - * @return signed data - */ - def sign(data: String, secret: String): String - - /** - * Validates signature. - * - * @param data the data to validate signature for - * @param signature the signature to validate - * @param secret the secret to use for validation - * @return true if signature is valid, otherwise returns false - */ - def validate(data: String, signature: String, secret: String): Boolean -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala index bcf9b35..ed6a890 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala @@ -5,34 +5,59 @@ import javax.crypto.spec.SecretKeySpec import org.janjaali.sprayjwt.encoder.{Base64Encoder, ByteEncoder} -/** - * Represents Hmac hashing algorithms. - * - * @param name the name of the hashing algorithm +/** Hash-based Message Authentication Codes (HMACs) algorithm to sign and + * validate digital signatures. */ -private[sprayjwt] abstract class HmacAlgorithm(override val name: String) extends HashingAlgorithm(name) { +sealed trait HmacAlgorithm extends Algorithm { private val provider = "SunJCE" - /** - * Hashing algorithm name used by SunJCE/BouncyCastle. - */ - protected val cryptoAlgName: String + protected def hashingAlgorithmName: String + // TODO: Check implementation override def sign(data: String, secret: String): String = { + val secretAsByteArray = ByteEncoder.getBytes(secret) - val secretKey = new SecretKeySpec(secretAsByteArray, cryptoAlgName) + val secretKey = new SecretKeySpec(secretAsByteArray, hashingAlgorithmName) val dataAsByteArray = ByteEncoder.getBytes(data) - val mac = Mac.getInstance(cryptoAlgName, provider) + val mac = Mac.getInstance(hashingAlgorithmName, provider) mac.init(secretKey) val signAsByteArray = mac.doFinal(dataAsByteArray) Base64Encoder.encode(signAsByteArray) } - override def validate(data: String, signature: String, secret: String): Boolean = { + // TODO: Check implementation + override def validate( + signature: String, + data: String, + secret: String + ): Boolean = { sign(data, secret) == signature } +} + +object HmacAlgorithm { + + case object Hs256 extends HmacAlgorithm { + + override val name: String = "HS256" + + override val hashingAlgorithmName = "HMACSHA256" + } + case object Hs384 extends HmacAlgorithm { + + override val name: String = "HS384" + + override val hashingAlgorithmName = "HMACSHA384" + } + + case object Hs512 extends HmacAlgorithm { + + override val name: String = "HS512" + + override val hashingAlgorithmName = "HMACSHA512" + } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala deleted file mode 100644 index de74f48..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS256.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents RS256 hashing algorithm. - */ -case object RS256 extends RsaAlgorithm("RS256") { - - override protected val cryptoAlgName = "SHA256withRSA" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala deleted file mode 100644 index b66919f..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS384.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents RS384 hashing algorithm. - */ -case object RS384 extends RsaAlgorithm("RS384") { - - override protected val cryptoAlgName = "SHA384withRSA" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala deleted file mode 100644 index 5182fbf..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RS512.scala +++ /dev/null @@ -1,10 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -/** - * Represents RS512 hashing algorithm. - */ -case object RS512 extends RsaAlgorithm("RS512") { - - override protected val cryptoAlgName = "SHA512withRSA" - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala index ed73711..741588e 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala @@ -8,38 +8,41 @@ import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} -/** - * Represents RSA hashing algorithms. - * - * @param name the name of the hashing algorithm +/** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign + * and validate digital signatures. */ -private[sprayjwt] abstract class RsaAlgorithm(override val name: String) extends HashingAlgorithm(name) { +sealed trait RsaAlgorithm extends Algorithm { private val provider = "BC" - /** - * Hashing algorithm name used by SunJCE/BouncyCastle. - */ - protected val cryptoAlgName: String + protected def hashingAlgorithmName: String + // TODO: Check implementation override def sign(data: String, secret: String): String = { + val key = getPrivateKey(secret) val dataByteArray = ByteEncoder.getBytes(data) - val signature = Signature.getInstance(cryptoAlgName, provider) + val signature = Signature.getInstance(hashingAlgorithmName, provider) signature.initSign(key) signature.update(dataByteArray) val signatureByteArray = signature.sign Base64Encoder.encode(signatureByteArray) } - override def validate(data: String, signature: String, secret: String): Boolean = { + // TODO: Check implementation + override def validate( + data: String, + signature: String, + secret: String + ): Boolean = { + val key = getPublicKey(secret) val dataByteArray = ByteEncoder.getBytes(data) - val rsaSignature = Signature.getInstance(cryptoAlgName, provider) + val rsaSignature = Signature.getInstance(hashingAlgorithmName, provider) rsaSignature.initVerify(key) rsaSignature.update(dataByteArray) rsaSignature.verify(Base64Decoder.decode(signature)) @@ -53,7 +56,7 @@ private[sprayjwt] abstract class RsaAlgorithm(override val name: String) extends case Some(publicKeyInfo: SubjectPublicKeyInfo) => val converter = new JcaPEMKeyConverter converter.getPublicKey(publicKeyInfo) - case _ => throw new IOException(s"Invalid key for $cryptoAlgName") + case _ => throw new IOException(s"Invalid key for $hashingAlgorithmName") } } @@ -65,8 +68,31 @@ private[sprayjwt] abstract class RsaAlgorithm(override val name: String) extends case Some(keyPair: PEMKeyPair) => val converter = new JcaPEMKeyConverter converter.getKeyPair(keyPair).getPrivate - case _ => throw new IOException(s"Invalid key for $cryptoAlgName") + case _ => throw new IOException(s"Invalid key for $hashingAlgorithmName") } } +} + +object RsaAlgorithm { + + case object Rs256 extends RsaAlgorithm { + + override val name: String = "RS256" + + override protected def hashingAlgorithmName: String = "SHA256withRSA" + } + case object Rs384 extends RsaAlgorithm { + + override val name: String = "RS384" + + override protected def hashingAlgorithmName: String = "SHA384withRSA" + } + + case object Rs512 extends RsaAlgorithm { + + override val name: String = "RS512" + + override protected def hashingAlgorithmName: String = "SHA512withRSA" + } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala index b32c805..cfc82cc 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala @@ -1,16 +1,15 @@ package org.janjaali.sprayjwt.encoder import java.util.Base64 +import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -/** - * Base64Encoder utility class. +/** Base64Encoder utility class. */ private[sprayjwt] object Base64Encoder { private lazy val base64Encoder: Base64.Encoder = Base64.getEncoder - /** - * Encodes text to a Base64 encoded String. + /** Encodes text to a Base64 encoded String. * * @param text the text to encode * @return Base64 encoded String @@ -20,8 +19,21 @@ private[sprayjwt] object Base64Encoder { encode(textAsByteArray) } - /** - * Encodes a ByteArray as String. + /** Encodes JSON value to a Base64 encoded String. + * + * @param jsonValue json value that should be encoded + * @param jsonStringSerializer Serializer that is used to serialize the json + * value to a String before Base64 encoding sets + * in + * @return Base64 encoded JSON value + */ + def encode( + jsonValue: JsonValue + )(implicit jsonStringSerializer: JsonStringSerializer): String = { + jsonStringSerializer.serialize(jsonValue) + } + + /** Encodes a ByteArray as String. * * @param byteArray the ByteArray to encode as String * @return String encoded ByteArray @@ -29,5 +41,4 @@ private[sprayjwt] object Base64Encoder { def encode(byteArray: Array[Byte]): String = { base64Encoder.encodeToString(byteArray).replaceAll("=", "") } - } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala index f1264c0..8c7c024 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/JwtHeader.scala @@ -1,13 +1,13 @@ package org.janjaali.sprayjwt.headers -import org.janjaali.sprayjwt.algorithms.HashingAlgorithm +import org.janjaali.sprayjwt.algorithms.Algorithm /** * Represents JWT-Header. * * @param algorithm the hashing algorithm which is used for encoding the JWT */ -case class JwtHeader(algorithm: HashingAlgorithm) { +case class JwtHeader(algorithm: Algorithm) { /** * Default type. */ diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala index 4cdecca..58d0a86 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala @@ -1,6 +1,6 @@ package org.janjaali.sprayjwt -import org.janjaali.sprayjwt.algorithms.HashingAlgorithm +import org.janjaali.sprayjwt.algorithms.Algorithm import org.janjaali.sprayjwt.exceptions.{InvalidJwtAlgorithmException, InvalidJwtHeaderException} import spray.json.{JsObject, JsString, JsValue, JsonReader, JsonWriter} @@ -28,7 +28,7 @@ package object headers { override def read(json: JsValue): JwtHeader = { json.asJsObject.getFields("alg", "typ") match { case Seq(JsString(alg), JsString(typ)) if typ == "JWT" => - HashingAlgorithm(alg) match { + Algorithm.forName(alg) match { case Some(algorithm) => JwtHeader(algorithm) case _ => throw new InvalidJwtAlgorithmException(s"Unsupported JWT algorithm $alg") } @@ -36,5 +36,4 @@ package object headers { } } } - -} +} // TODO: Many test are probably failing, Fix them! diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala new file mode 100644 index 0000000..14a1d65 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala @@ -0,0 +1,90 @@ +package org.janjaali.sprayjwt.json + +/** Commonly used JSON writers for commonly used types. + */ +object CommonJsonWriters { + + /** Implicitly usable CommonJsonWriters. + */ + object Implicits { + + /** Constructs JSON writer for Int types. + * + * @return JSON writer + */ + implicit def intJsonWriter: JsonWriter[Int] = { + CommonJsonWriters.intJsonWriter + } + + /** Constructs JSON writer for Long types. + * + * @return JSON writer + */ + implicit def longJsonWriter: JsonWriter[Long] = { + CommonJsonWriters.longJsonWriter + } + + /** Constructs JSON writer for String types. + * + * @return JSON writer + */ + implicit def stringJsonWriter: JsonWriter[String] = { + CommonJsonWriters.stringJsonWriter + } + } + + /** Constructs JSON writer for Int types. + * + * @return JSON writer + */ + def intJsonWriter: JsonWriter[Int] = { + jsonNumberWriter[Int] + } + + /** Constructs JSON writer for Long types. + * + * @return JSON writer + */ + def longJsonWriter: JsonWriter[Long] = { + jsonNumberWriter[Long] + } + + /** Constructs JSON writer for String types. + * + * @return JSON writer + */ + def stringJsonWriter: JsonWriter[String] = { + new JsonWriter[String] { + override def write(value: String): JsonValue = { + JsonString(value) + } + } + } + + /** Constructs flat JSON writer for product types with at least an arity of 1. + * + * Ignores all other elements for JSON writing than the first element. + * + * @return JSON writer + */ + def flatValueJsonWriter[P <: Product, T: JsonWriter]: JsonWriter[P] = { + new JsonWriter[P] { + def write(product: P): JsonValue = { + val valueWriter = implicitly[JsonWriter[T]] + valueWriter.write(product.productElement(0).asInstanceOf[T]) + } + } + } + + /** Constructs JSON writer (i.e. JSON number) for numeric types. + * + * @return JSON writer + */ + private def jsonNumberWriter[T: Numeric]: JsonWriter[T] = { + new JsonWriter[T] { + override def write(value: T): JsonValue = { + JsonNumber(value) + } + } + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala new file mode 100644 index 0000000..027779f --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -0,0 +1,9 @@ +package org.janjaali.sprayjwt.json + + +// TODO: Not tested yet! + +trait JsonStringSerializer { + + def serialize(jsonObject: JsonValue): String +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala new file mode 100644 index 0000000..3ad77f2 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -0,0 +1,9 @@ +package org.janjaali.sprayjwt.json + +sealed trait JsonValue + +case class JsonObject(members: Map[String, JsonValue]) extends JsonValue + +final case class JsonString(value: String) extends JsonValue + +final case class JsonNumber[T: Numeric](value: T) extends JsonValue diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonWriter.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonWriter.scala new file mode 100644 index 0000000..33054f8 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonWriter.scala @@ -0,0 +1,13 @@ +package org.janjaali.sprayjwt.json + +/** Provides a JSON writer for a given type T. + */ +trait JsonWriter[T] { + + /** Writes a given value as JSON value. + * + * @param value value that should be written as JSON value + * @return JSON value + */ + def write(value: T): JsonValue +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala new file mode 100644 index 0000000..9d35edd --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala @@ -0,0 +1,70 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.{JsonValue, JsonWriter} + +/** A claim consists of a name-value pair that provide information about a token + * subject. + */ +trait Claim { + + /** Type of this claim. + */ + protected type T + + /** Json writer for this claim's value. + */ + protected def valueJsonWriter: JsonWriter[T] + + /** Name of this claim. + * + * @return claim name + */ + def name: String + + /** Value of this claim. + * + * @return claim value + */ + def value: T + + /** JSON representation of this claim's value. + * + * @return JSON value + */ + private[sprayjwt] def valueAsJson: JsonValue = { + valueJsonWriter.write(this.value) + } +} + +object Claim { + + /** The "exp" (expiration time) claim identifies the expiration time on or + * after which the JWT MUST NOT be accepted for processing. + * + * @param value seconds since the epoch + */ + final case class ExpirationTime(value: NumericDate) extends Claim { + + override type T = NumericDate + + override def valueJsonWriter: JsonWriter[T] = NumericDate.jsonWriter + + override def name: String = "exp" + } + + /** Represents a private claim that producer and consumer of a JWT can agree + * to use freely unlike registered or public claims. + * + * @param name name of this claim + * @param value value of this claim + */ + final case class Private[C: JsonWriter]( + name: String, + value: C + ) extends Claim { + + override type T = C + + override def valueJsonWriter: JsonWriter[T] = implicitly[JsonWriter[T]] + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala new file mode 100644 index 0000000..04f023d --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala @@ -0,0 +1,34 @@ +package org.janjaali.sprayjwt.jwt.model + +/** Claims Set represents a data structure whose members are the claims. The + * claim names within a Claims Set MUST be unique. + * + * @param claims uniquely named claims + */ +sealed abstract case class ClaimsSet private (claims: Set[Claim]) + +object ClaimsSet { + + /** Constructs a Claim Set for a set of uniquely named claims. + * + * Claims with duplicated claim names override pre-existing claims in the + * same set. + * + * @param claims uniquely named claims + * @return Claim Set + */ + def apply(claims: Seq[Claim]): ClaimsSet = { + + val claimsWithUniqueNames = { + claims.foldRight(List.empty[Claim]) { case (claim, claims) => + if (claims.map(_.name).contains(claim.name)) { + claims + } else { + claim :: claims + } + } + } + + new ClaimsSet(claimsWithUniqueNames.toSet) {} + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala new file mode 100644 index 0000000..05dcec1 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala @@ -0,0 +1,22 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.JsonObject + +/** JWS Payload consisting of a Claims Set. + * + * @param claims claims of this JWS Payload + */ +final case class JwsPayload(claims: ClaimsSet) { + + /** JSON representation of this JWS payload. + * + * @return JSON object + */ + def asJson: JsonObject = { + JsonObject( + claims.claims.map { claim => + claim.name -> claim.valueAsJson + }.toMap + ) + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala new file mode 100644 index 0000000..6f8130c --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala @@ -0,0 +1,25 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.{CommonJsonWriters, JsonWriter} + +/** A JSON numeric value representing the number of seconds from + * 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap + * seconds. + * + * @param value seconds since the epoch + */ +final case class NumericDate(secondsSinceEpoch: Long) + +/** Companion object for Numeric Date. + */ +object NumericDate { + + /** JSON writer for Numeric Date. + */ + val jsonWriter: JsonWriter[NumericDate] = { + + import CommonJsonWriters.Implicits.longJsonWriter + + CommonJsonWriters.flatValueJsonWriter + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala index 3d4616e..bc6c5cc 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala @@ -1,10 +1,10 @@ package org.janjaali.sprayjwt import org.janjaali.sprayjwt.algorithms._ -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec import spray.json.{JsBoolean, JsObject, JsString} -class JwtSpec extends FunSpec { +class JwtSpec extends AnyFunSpec { describe("Jwt encodes the header to JSON") { describe("HS256") { @@ -12,10 +12,11 @@ class JwtSpec extends FunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, HS256).get + val jwt = LegacyJwt.encode(payload, secret, HS256).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" + val expectedJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" // scalastyle:on assert(jwt == expectedJwt) @@ -23,10 +24,13 @@ class JwtSpec extends FunSpec { it("encodes as JWT with iss") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, HS256, JwtClaims(iss = Some("issuer"))).get + val jwt = LegacyJwt + .encode(payload, secret, HS256, JwtClaims(iss = Some("issuer"))) + .get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" + val expectedJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" // scalastyle:on assert(jwt == expectedJwt) @@ -40,10 +44,11 @@ class JwtSpec extends FunSpec { "admin" -> JsBoolean(true) ) - val jwt = Jwt.encode(jsValue, secret, HS256).get + val jwt = LegacyJwt.encode(jsValue, secret, HS256).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" + val expectedJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" // scalastyle:on assert(jwt == expectedJwt) @@ -57,10 +62,13 @@ class JwtSpec extends FunSpec { "admin" -> JsBoolean(true) ) - val jwt = Jwt.encode(jsValue, secret, HS256, JwtClaims(iss = Some("issuer"))).get + val jwt = LegacyJwt + .encode(jsValue, secret, HS256, JwtClaims(iss = Some("issuer"))) + .get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" + val expectedJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" // scalastyle:on assert(jwt == expectedJwt) @@ -71,18 +79,26 @@ class JwtSpec extends FunSpec { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, HS256, JwtClaims( - iss = Some("issuer"), - sub = Some("subject"), - aud = Some(Set("audience")), - isa = Some(500), - exp = Some(1000), - nbf = Some(2000), - iat = Some(3000), - jti = Some("jwtId") - )).get - - val expectedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjIwMDAsImFkbWluIjp0cnVlLCJuYW1lIjoiSm9obiBEb2UiLCJqdGkiOiJqd3RJZCIsImV4cCI6MTAwMCwiaXNhIjo1MDAsImlhdCI6MzAwMCwic3ViIjoic3ViamVjdCIsImF1ZCI6ImF1ZGllbmNlIiwiaXNzIjoiaXNzdWVyIn0.2zS7vqKCLPKOlre6LYMMR/dTp41Q9jV5KiEyE9I6JLw" + val jwt = LegacyJwt + .encode( + payload, + secret, + HS256, + JwtClaims( + iss = Some("issuer"), + sub = Some("subject"), + aud = Some(Set("audience")), + isa = Some(500), + exp = Some(1000), + nbf = Some(2000), + iat = Some(3000), + jti = Some("jwtId") + ) + ) + .get + + val expectedJwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjIwMDAsImFkbWluIjp0cnVlLCJuYW1lIjoiSm9obiBEb2UiLCJqdGkiOiJqd3RJZCIsImV4cCI6MTAwMCwiaXNhIjo1MDAsImlhdCI6MzAwMCwic3ViIjoic3ViamVjdCIsImF1ZCI6ImF1ZGllbmNlIiwiaXNzIjoiaXNzdWVyIn0.2zS7vqKCLPKOlre6LYMMR/dTp41Q9jV5KiEyE9I6JLw" // scalastyle:on assert(jwt == expectedJwt) @@ -90,10 +106,11 @@ class JwtSpec extends FunSpec { it("decodes JWT as String") { // scalastyle:off - val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" + val token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" // scalastyle:on - val decodedPayload = Jwt.decodeAsString(token, secret).get + val decodedPayload = LegacyJwt.decodeAsString(token, secret).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decodedPayload == expected) @@ -101,10 +118,11 @@ class JwtSpec extends FunSpec { it("decodes JWT as JsValue") { // scalastyle:off - val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" + val token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" // scalastyle:on - val json = Jwt.decode(token, secret).get + val json = LegacyJwt.decode(token, secret).get val expected = JsObject( "sub" -> JsString("1234567890"), @@ -116,10 +134,11 @@ class JwtSpec extends FunSpec { it("decodes JWT as JsValue with iss claim") { // scalastyle:off - val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" + val token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" // scalastyle:on - val json = Jwt.decode(token, secret).get + val json = LegacyJwt.decode(token, secret).get val expected = JsObject( "sub" -> JsString("1234567890"), @@ -136,10 +155,11 @@ class JwtSpec extends FunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, HS384).get + val jwt = LegacyJwt.encode(payload, secret, HS384).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" + val expectedJwt = + "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" // scalastyle:on assert(jwt == expectedJwt) @@ -147,10 +167,11 @@ class JwtSpec extends FunSpec { it("decodes JWT as String") { // scalastyle:off - val token = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" + val token = + "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" // scalastyle:on - val decodedPayload = Jwt.decodeAsString(token, secret).get + val decodedPayload = LegacyJwt.decodeAsString(token, secret).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decodedPayload == expected) @@ -162,10 +183,11 @@ class JwtSpec extends FunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, HS512).get + val jwt = LegacyJwt.encode(payload, secret, HS512).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.YI0rUGDq5XdRw8vW2sDLRNFMN8Waol03iSFH8I4iLzuYK7FKHaQYWzPt0BJFGrAmKJ6SjY0mJIMZqNQJFVpkuw" + val expectedJwt = + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.YI0rUGDq5XdRw8vW2sDLRNFMN8Waol03iSFH8I4iLzuYK7FKHaQYWzPt0BJFGrAmKJ6SjY0mJIMZqNQJFVpkuw" // scalastyle:on assert(jwt == expectedJwt) @@ -173,10 +195,11 @@ class JwtSpec extends FunSpec { it("decodes JWT as String") { // scalastyle:off - val token = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" + val token = + "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" // scalastyle:on - val decodedPayload = Jwt.decodeAsString(token, secret).get + val decodedPayload = LegacyJwt.decodeAsString(token, secret).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decodedPayload == expected) @@ -185,28 +208,36 @@ class JwtSpec extends FunSpec { describe("RS256") { it("encodes as JWT") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) + val secret = + try source.mkString + finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, RS256).get + val jwt = LegacyJwt.encode(payload, secret, RS256).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" + val expectedJwt = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" // scalastyle:on assert(jwt == expectedJwt) } it("decodes JWT as String") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) + val public = + try source.mkString + finally source.close // scalastyle:off - val token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" + val token = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" // scalastyle:on - val decoded = Jwt.decodeAsString(token, public).get + val decoded = LegacyJwt.decodeAsString(token, public).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decoded == expected) @@ -215,28 +246,36 @@ class JwtSpec extends FunSpec { describe("RS384") { it("encodes as JWT") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) + val secret = + try source.mkString + finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, RS384).get + val jwt = LegacyJwt.encode(payload, secret, RS384).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" + val expectedJwt = + "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" // scalastyle:on assert(jwt == expectedJwt) } it("decodes JWT as String") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) + val public = + try source.mkString + finally source.close // scalastyle:off - val token = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" + val token = + "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" // scalastyle:on - val decoded = Jwt.decodeAsString(token, public).get + val decoded = LegacyJwt.decodeAsString(token, public).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decoded == expected) @@ -245,28 +284,36 @@ class JwtSpec extends FunSpec { describe("RS512") { it("encodes as JWT") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) + val secret = + try source.mkString + finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = Jwt.encode(payload, secret, RS512).get + val jwt = LegacyJwt.encode(payload, secret, RS512).get // scalastyle:off - val expectedJwt = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" + val expectedJwt = + "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" // scalastyle:on assert(jwt == expectedJwt) } it("decodes JWT as String") { - val source = scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = try source.mkString finally source.close + val source = + scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) + val public = + try source.mkString + finally source.close // scalastyle:off - val token = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" + val token = + "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" // scalastyle:on - val decoded = Jwt.decodeAsString(token, public).get + val decoded = LegacyJwt.decodeAsString(token, public).get val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" assert(decoded == expected) diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala index a38a690..677e8ca 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala @@ -1,27 +1,27 @@ package org.janjaali.sprayjwt.algorithms -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec -class HashingAlgorithmSpec extends FunSpec { +class HashingAlgorithmSpec extends AnyFunSpec { describe("HashingAlgorithm") { describe("maps Strings to HashingAlgorithm") { it("maps HS256") { - HashingAlgorithm("HS256") match { + Algorithm("HS256") match { case Some(alg) => assert(alg == HS256) case _ => fail } } it("maps HS384") { - HashingAlgorithm("HS384") match { + Algorithm("HS384") match { case Some(alg) => assert(alg == HS384) case _ => fail } } it("maps HS512") { - HashingAlgorithm("HS512") match { + Algorithm("HS512") match { case Some(alg) => assert(alg == HS512) case _ => fail } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala index 0f7d229..9c558e1 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala @@ -1,8 +1,8 @@ package org.janjaali.sprayjwt.encoder -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec -class Base64DecoderSpec extends FunSpec { +class Base64DecoderSpec extends AnyFunSpec { describe("Base64Decoder") { it("decodes text as Base64 decoded byte-array") { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala index b59d30b..cb9405f 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala @@ -1,8 +1,8 @@ package org.janjaali.sprayjwt.encoder -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec -class Base64EncoderSpec extends FunSpec { +class Base64EncoderSpec extends AnyFunSpec { describe("Base64Encoder") { it("encodes text as Base64 encoded text") { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala index dadd364..e2e7015 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/ByteEncoderSpec.scala @@ -1,8 +1,8 @@ package org.janjaali.sprayjwt.encoder -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec -class ByteEncoderSpec extends FunSpec { +class ByteEncoderSpec extends AnyFunSpec { describe("ByteEncoder") { it("encodes text as byte array") { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala index 6c25539..763016c 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala @@ -1,10 +1,10 @@ package org.janjaali.sprayjwt.headers import org.janjaali.sprayjwt.algorithms.HS256 -import org.scalatest.FunSpec +import org.scalatest.funspec.AnyFunSpec import spray.json._ -class JwtHeaderJsonProtocolSpec extends FunSpec { +class JwtHeaderJsonProtocolSpec extends AnyFunSpec { describe("JwtHeaderJsonProtocol trait") { it("converts JWT-Header to JsValue") { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/json/CommonJsonWritersSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/json/CommonJsonWritersSpec.scala new file mode 100644 index 0000000..b3543de --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/json/CommonJsonWritersSpec.scala @@ -0,0 +1,48 @@ +package org.janjaali.sprayjwt.json + +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +class CommonJsonWritersSpec extends ScalaTestSpec { + + private val sut = CommonJsonWriters + + "Int JsonWriter" - { + + "should write Int values as JsonNumber." in { + + sut.intJsonWriter.write(2) shouldBe JsonNumber(2) + } + } + + "Long JsonWriter" - { + + "should write Long values as JsonNumber." in { + + sut.longJsonWriter.write(41L) shouldBe JsonNumber(41L) + } + } + + "String JsonWriter" - { + + "should write String values as JsonString." in { + + sut.stringJsonWriter.write("string") shouldBe JsonString("string") + } + } + + "Flat value JSON writer" - { + + "should write flat JSON values for products with at least an arbitrary of 1." in { + + import CommonJsonWriters.Implicits.stringJsonWriter + + case class Prod(value: String) + + val prodExample = Prod("dance") + + sut.flatValueJsonWriter[Prod, String].write(prodExample) shouldBe { + JsonString("dance") + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala new file mode 100644 index 0000000..4c064ff --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala @@ -0,0 +1,18 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.JsonNumber +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks + +class ClaimSpec extends ScalaTestSpec with ScalaCheckPropertyChecks { + + "Expiration time claim" - { + + "value should be JSON serialized as a JSON number." in { + + forAll(ScalaCheckGenerators.expirationTimeClaimGen) { claim => + claim.valueAsJson shouldBe JsonNumber(claim.value.secondsSinceEpoch) + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala new file mode 100644 index 0000000..710f08d --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala @@ -0,0 +1,47 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ +import org.scalacheck.Gen +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class ClaimsSetSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "Claim Set" - { + + "when constructed with claims that do not share same claim names" - { + + "should add all claims." in { + + val claimsWithDistinctNames = { + Gen + .listOf(ScalaCheckGenerators.claimGen) + .map(_.groupBy(_.name)) + .map(_.mapValues(_.headOption)) + .map(_.values.flatten) + .map(_.toSeq) + } + + forAll(claimsWithDistinctNames) { claims => + ClaimsSet(claims).claims should contain theSameElementsAs claims + } + } + } + + "when constructed with claims that share same claim names" - { + + val claim1 = Claim.Private("name", 1) + val claim2 = Claim.Private("name", 2) + + val claims = List( + claim1, + claim2 + ) + + "should only add the later ones." in { + + ClaimsSet(claims).claims should contain only claim2 + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala new file mode 100644 index 0000000..70f5ffa --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala @@ -0,0 +1,26 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class JwsPayloadSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "JWS payload" - { + + "should serialize claims as JSON object." in { + + forAll(ScalaCheckGenerators.jwsPayloadGen) { jwsPayload => + val jwsPayloadAsJsonObject = jwsPayload.asJson + + val expectedJsonObjectMembers = { + + jwsPayload.claims.claims.map { claim => + claim.name -> claim.valueAsJson + }.toMap + } + + jwsPayloadAsJsonObject.members should contain theSameElementsAs expectedJsonObjectMembers + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala new file mode 100644 index 0000000..894daab --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala @@ -0,0 +1,25 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.JsonNumber +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class NumericDateSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "Numeric date" - { + + "JSON writer" - { + + val sut = NumericDate.jsonWriter + + "should writer numeric dates as JSON number." in { + + forAll(ScalaCheckGenerators.numericDateGen) { numericDate => + sut.write(numericDate) shouldBe { + JsonNumber(numericDate.secondsSinceEpoch) + } + } + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala new file mode 100644 index 0000000..4632a99 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala @@ -0,0 +1,65 @@ +package org.janjaali.sprayjwt.jwt.model + +import org.janjaali.sprayjwt.json.CommonJsonWriters +import org.scalacheck.Gen + +import java.time.Instant + +private[jwt] object ScalaCheckGenerators { + + /** Generator for numeric dates containing epoch seconds in the time frame of: + * [now - 100 years, now + 100 years]. + * + * @return generator for numeric date + */ + def numericDateGen: Gen[NumericDate] = { + + import scala.concurrent.duration._ + + val aHundredYearsInSeconds = (365.days * 10).toSeconds + + val now = Instant.now() + + Gen + .chooseNum( + now.minusSeconds(aHundredYearsInSeconds).getEpochSecond(), + now.plusSeconds(aHundredYearsInSeconds).getEpochSecond() + ) + .map(NumericDate.apply) + } + + /** Generator for an expiration time claim that contains a numeric date time + * in the time interval of: [now - 100 years, now + 100 years]. + * + * @return generator for expiration time + */ + def expirationTimeClaimGen: Gen[Claim.ExpirationTime] = { + numericDateGen.map(Claim.ExpirationTime) + } + + def privateClaimGen: Gen[Claim.Private[_]] = { + + import CommonJsonWriters.Implicits.stringJsonWriter + + for { + name <- Gen.alphaStr + value <- Gen.alphaStr + } yield Claim.Private(name, value) + } + + def claimGen: Gen[Claim] = { + + Gen.oneOf( + expirationTimeClaimGen, + privateClaimGen + ) + } + + def claimsSetGen: Gen[ClaimsSet] = { + Gen.listOf(claimGen).map(ClaimsSet.apply) + } + + def jwsPayloadGen: Gen[JwsPayload] = { + claimsSetGen.map(JwsPayload.apply) + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala new file mode 100644 index 0000000..6f19f47 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala @@ -0,0 +1,6 @@ +package org.janjaali.sprayjwt.tests + +import org.scalatest.matchers.should.Matchers +import org.scalatest.freespec.AnyFreeSpec + +trait ScalaTestSpec extends AnyFreeSpec with Matchers From f772d6301c8e2d18d154e875441c67df9241c394 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Wed, 24 Mar 2021 18:41:53 +0100 Subject: [PATCH 23/43] WIP. --- .../sprayjwt/akkahttp/token/TokenRoutes.scala | 4 +- .../scala/org/janjaali/sprayjwt/Jwt.scala | 8 +- .../sprayjwt/algorithms/Algorithm.scala | 169 +++++++++++++++--- .../sprayjwt/algorithms/HmacAlgorithm.scala | 63 ------- .../sprayjwt/algorithms/RsaAlgorithm.scala | 98 ---------- .../janjaali/sprayjwt/headers/headers.scala | 34 +++- .../janjaali/sprayjwt/jws/JoseHeader.scala | 155 ++++++++++++++++ .../org/janjaali/sprayjwt/jws/Signature.scala | 5 + .../sprayjwt/jwt/{model => }/Claim.scala | 2 +- .../org/janjaali/sprayjwt/jwt/ClaimsSet.scala | 30 ++++ .../sprayjwt/jwt/{model => }/JwsPayload.scala | 2 +- .../jwt/{model => }/NumericDate.scala | 2 +- .../sprayjwt/jwt/model/ClaimsSet.scala | 34 ---- .../sprayjwt/util/CollectionsFactory.scala | 36 ++++ .../scala/org/janjaali/sprayjwt/JwtSpec.scala | 30 ++-- .../sprayjwt/algorithms/AlgorithmSpec.scala | 8 + .../algorithms/HashingAlgorithmSpec.scala | 32 ---- .../algorithms/ScalaCheckGenerators.scala | 29 +++ .../headers/JwtHeaderJsonProtocolSpec.scala | 14 +- .../sprayjwt/jws/JoseHeaderSpec.scala | 101 +++++++++++ .../sprayjwt/jws/ScalaCheckGenerators.scala | 64 +++++++ .../sprayjwt/jwt/{model => }/ClaimSpec.scala | 2 +- .../janjaali/sprayjwt/jwt/ClaimsSetSpec.scala | 57 ++++++ .../jwt/{model => }/JwsPayloadSpec.scala | 2 +- .../jwt/{model => }/NumericDateSpec.scala | 2 +- .../{model => }/ScalaCheckGenerators.scala | 8 +- .../sprayjwt/jwt/model/ClaimsSetSpec.scala | 47 ----- .../tests/ScalaCheckGeneratorsSampler.scala | 21 +++ .../util/CollectionsFactorySpec.scala | 38 ++++ 29 files changed, 766 insertions(+), 331 deletions(-) delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala rename spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/{model => }/Claim.scala (97%) create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala rename spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/{model => }/JwsPayload.scala (91%) rename spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/{model => }/NumericDate.scala (93%) delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/util/CollectionsFactory.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/{model => }/ClaimSpec.scala (92%) create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/{model => }/JwsPayloadSpec.scala (94%) rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/{model => }/NumericDateSpec.scala (93%) rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/{model => }/ScalaCheckGenerators.scala (91%) delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/util/CollectionsFactorySpec.scala diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala index 355051d..303d561 100644 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala +++ b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala @@ -2,8 +2,8 @@ package org.janjaali.sprayjwt.akkahttp.token import akka.http.scaladsl.server.{Directives, Route} import org.janjaali.sprayjwt.LegacyJwt -import org.janjaali.sprayjwt.algorithms.HS256 import spray.json.{JsObject, JsString} +import org.janjaali.sprayjwt.algorithms.Algorithm class TokenRoutes(secret: String) extends Directives { val routes: Route = Route { @@ -11,7 +11,7 @@ class TokenRoutes(secret: String) extends Directives { rejectEmptyResponse { get { val payload = JsObject("dance" -> JsString("in the rain")) - val jwt = LegacyJwt.encode(payload, secret, HS256) + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs256) complete(jwt) } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala index 083e903..b2c8994 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala @@ -15,8 +15,8 @@ import spray.json._ import scala.util.{Success, Try} import org.janjaali.sprayjwt.json._ -import org.janjaali.sprayjwt.jwt.model.ClaimsSet -import org.janjaali.sprayjwt.jwt.model.JwsPayload +import org.janjaali.sprayjwt.jwt.ClaimsSet +import org.janjaali.sprayjwt.jwt.JwsPayload /** TODO: */ @@ -79,7 +79,7 @@ private case class JoseHeader(algorithm: Algorithm) { JsonObject( Map( "typ" -> JsonString(this.typ), - "alg" -> JsonString(algorithm.name) + "alg" -> JsonString("algorithm.name") // TODO: JSON Writer for JoseHeader needed ) ) } @@ -222,7 +222,7 @@ object LegacyJwt { val algorithm = getAlgorithmFromHeader(header) - if (!algorithm.validate(data, signature, secret)) { + if (!algorithm.validate(signature, data, secret)) { throw new InvalidSignatureException() } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 2f09e70..e353752 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -1,14 +1,18 @@ package org.janjaali.sprayjwt.algorithms -/** Represents a cryptographic algorithms and identifiers used with JWT. - */ -private[sprayjwt] trait Algorithm { +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} +import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} - /** Name of this algorithm. - * - * @return name - */ - def name: String +import java.io.{IOException, StringReader} +import java.security.{PrivateKey, PublicKey, Signature} + +/** Represents a cryptographic algorithm used with JWT. + */ +sealed trait Algorithm { // TODO: Maybe a better suited signature // Digitally signs the contents of the given JWS Header and the JWS Payload. @@ -22,6 +26,9 @@ private[sprayjwt] trait Algorithm { */ def sign(data: String, secret: String): String + // TODO: Idea for new method signature + // def sign(jwsProtectedHeader: JwsProtectedHeader, jwsPayload: JwsPayload): JwsSignature + /** Validates signature. // TODO: legacy? * * @param signature the signature to validate @@ -32,27 +39,141 @@ private[sprayjwt] trait Algorithm { def validate(signature: String, data: String, secret: String): Boolean } -/** Companion object to map Strings as hashing algorithms. +/** Provides algorithms. */ object Algorithm { - private val all = { - List( - HmacAlgorithm.Hs256, - HmacAlgorithm.Hs384, - HmacAlgorithm.Hs512, - RsaAlgorithm.Rs256, - RsaAlgorithm.Rs384, - RsaAlgorithm.Rs512 - ) + /** Hash-based Message Authentication Codes (HMACs) algorithm to sign and + * validate digital signatures. + */ + sealed trait Hmac extends Algorithm { + + private val provider = "SunJCE" + + protected def hashingAlgorithmName: String + + // TODO: Check implementation + override def sign(data: String, secret: String): String = { + + val secretAsByteArray = ByteEncoder.getBytes(secret) + val secretKey = new SecretKeySpec(secretAsByteArray, hashingAlgorithmName) + + val dataAsByteArray = ByteEncoder.getBytes(data) + + val mac = Mac.getInstance(hashingAlgorithmName, provider) + mac.init(secretKey) + val signAsByteArray = mac.doFinal(dataAsByteArray) + Base64Encoder.encode(signAsByteArray) + } + + // TODO: Check implementation + override def validate( + signature: String, + data: String, + secret: String + ): Boolean = { + sign(data, secret) == signature + } } - /** Resolves a given algorithm name to an algorithm implementation. - * - * @param name name of algorithm - * @return algorithm + /** Provides HMAC algorithms. + */ + object Hmac { + + case object Hs256 extends Hmac { + override val hashingAlgorithmName = "HMACSHA256" + } + + case object Hs384 extends Hmac { + override val hashingAlgorithmName = "HMACSHA384" + } + + case object Hs512 extends Hmac { + override val hashingAlgorithmName = "HMACSHA512" + } + } + + /** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign + * and validate digital signatures. */ - def forName(name: String): Option[Algorithm] = { - Algorithm.all.find(_.name == name) + sealed trait Rsa extends Algorithm { + + private val provider = "BC" + + protected def hashingAlgorithmName: String + + // TODO: Check implementation + override def sign(data: String, secret: String): String = { + + val key = getPrivateKey(secret) + + val dataByteArray = ByteEncoder.getBytes(data) + + val signature = Signature.getInstance(hashingAlgorithmName, provider) + signature.initSign(key) + signature.update(dataByteArray) + val signatureByteArray = signature.sign + Base64Encoder.encode(signatureByteArray) + } + + // TODO: Check implementation + override def validate( + signature: String, + data: String, + secret: String + ): Boolean = { + + val key = getPublicKey(secret) + + val dataByteArray = ByteEncoder.getBytes(data) + + val rsaSignature = Signature.getInstance(hashingAlgorithmName, provider) + rsaSignature.initVerify(key) + rsaSignature.update(dataByteArray) + rsaSignature.verify(Base64Decoder.decode(signature)) + } + + private def getPublicKey(str: String): PublicKey = { + val pemParser = new PEMParser(new StringReader(str)) + val keyPair = pemParser.readObject() + + Option(keyPair) match { + case Some(publicKeyInfo: SubjectPublicKeyInfo) => + val converter = new JcaPEMKeyConverter + converter.getPublicKey(publicKeyInfo) + case _ => + throw new IOException(s"Invalid key for $hashingAlgorithmName") + } + } + + private def getPrivateKey(str: String): PrivateKey = { + val pemParser = new PEMParser(new StringReader(str)) + val keyPair = pemParser.readObject() + + Option(keyPair) match { + case Some(keyPair: PEMKeyPair) => + val converter = new JcaPEMKeyConverter + converter.getKeyPair(keyPair).getPrivate + case _ => + throw new IOException(s"Invalid key for $hashingAlgorithmName") + } + } + } + + /** Provides RSA algorithms. + */ + object Rsa { + + case object Rs256 extends Rsa { + override protected def hashingAlgorithmName: String = "SHA256withRSA" + } + + case object Rs384 extends Rsa { + override protected def hashingAlgorithmName: String = "SHA384withRSA" + } + + case object Rs512 extends Rsa { + override protected def hashingAlgorithmName: String = "SHA512withRSA" + } } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala deleted file mode 100644 index ed6a890..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/HmacAlgorithm.scala +++ /dev/null @@ -1,63 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -import org.janjaali.sprayjwt.encoder.{Base64Encoder, ByteEncoder} - -/** Hash-based Message Authentication Codes (HMACs) algorithm to sign and - * validate digital signatures. - */ -sealed trait HmacAlgorithm extends Algorithm { - - private val provider = "SunJCE" - - protected def hashingAlgorithmName: String - - // TODO: Check implementation - override def sign(data: String, secret: String): String = { - - val secretAsByteArray = ByteEncoder.getBytes(secret) - val secretKey = new SecretKeySpec(secretAsByteArray, hashingAlgorithmName) - - val dataAsByteArray = ByteEncoder.getBytes(data) - - val mac = Mac.getInstance(hashingAlgorithmName, provider) - mac.init(secretKey) - val signAsByteArray = mac.doFinal(dataAsByteArray) - Base64Encoder.encode(signAsByteArray) - } - - // TODO: Check implementation - override def validate( - signature: String, - data: String, - secret: String - ): Boolean = { - sign(data, secret) == signature - } -} - -object HmacAlgorithm { - - case object Hs256 extends HmacAlgorithm { - - override val name: String = "HS256" - - override val hashingAlgorithmName = "HMACSHA256" - } - - case object Hs384 extends HmacAlgorithm { - - override val name: String = "HS384" - - override val hashingAlgorithmName = "HMACSHA384" - } - - case object Hs512 extends HmacAlgorithm { - - override val name: String = "HS512" - - override val hashingAlgorithmName = "HMACSHA512" - } -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala deleted file mode 100644 index 741588e..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/RsaAlgorithm.scala +++ /dev/null @@ -1,98 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -import java.io.{IOException, StringReader} -import java.security.{PrivateKey, PublicKey, Signature} - -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter -import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} -import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} - -/** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign - * and validate digital signatures. - */ -sealed trait RsaAlgorithm extends Algorithm { - - private val provider = "BC" - - protected def hashingAlgorithmName: String - - // TODO: Check implementation - override def sign(data: String, secret: String): String = { - - val key = getPrivateKey(secret) - - val dataByteArray = ByteEncoder.getBytes(data) - - val signature = Signature.getInstance(hashingAlgorithmName, provider) - signature.initSign(key) - signature.update(dataByteArray) - val signatureByteArray = signature.sign - Base64Encoder.encode(signatureByteArray) - } - - // TODO: Check implementation - override def validate( - data: String, - signature: String, - secret: String - ): Boolean = { - - val key = getPublicKey(secret) - - val dataByteArray = ByteEncoder.getBytes(data) - - val rsaSignature = Signature.getInstance(hashingAlgorithmName, provider) - rsaSignature.initVerify(key) - rsaSignature.update(dataByteArray) - rsaSignature.verify(Base64Decoder.decode(signature)) - } - - private def getPublicKey(str: String): PublicKey = { - val pemParser = new PEMParser(new StringReader(str)) - val keyPair = pemParser.readObject() - - Option(keyPair) match { - case Some(publicKeyInfo: SubjectPublicKeyInfo) => - val converter = new JcaPEMKeyConverter - converter.getPublicKey(publicKeyInfo) - case _ => throw new IOException(s"Invalid key for $hashingAlgorithmName") - } - } - - private def getPrivateKey(str: String): PrivateKey = { - val pemParser = new PEMParser(new StringReader(str)) - val keyPair = pemParser.readObject() - - Option(keyPair) match { - case Some(keyPair: PEMKeyPair) => - val converter = new JcaPEMKeyConverter - converter.getKeyPair(keyPair).getPrivate - case _ => throw new IOException(s"Invalid key for $hashingAlgorithmName") - } - } -} - -object RsaAlgorithm { - - case object Rs256 extends RsaAlgorithm { - - override val name: String = "RS256" - - override protected def hashingAlgorithmName: String = "SHA256withRSA" - } - - case object Rs384 extends RsaAlgorithm { - - override val name: String = "RS384" - - override protected def hashingAlgorithmName: String = "SHA384withRSA" - } - - case object Rs512 extends RsaAlgorithm { - - override val name: String = "RS512" - - override protected def hashingAlgorithmName: String = "SHA512withRSA" - } -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala index 58d0a86..451228a 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala @@ -3,6 +3,12 @@ package org.janjaali.sprayjwt import org.janjaali.sprayjwt.algorithms.Algorithm import org.janjaali.sprayjwt.exceptions.{InvalidJwtAlgorithmException, InvalidJwtHeaderException} import spray.json.{JsObject, JsString, JsValue, JsonReader, JsonWriter} +import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs256 +import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs384 +import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs512 +import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs256 +import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs384 +import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs512 /** * Package object for headers package. @@ -14,8 +20,20 @@ package object headers { */ implicit object JwtHeaderJsonWriter extends JsonWriter[JwtHeader] { def write(jwtHeader: JwtHeader): JsValue = { + + // TODO: Extract hardcoded strings out + val algorithmIdentifier = jwtHeader.algorithm match { + case Rs256 => "RS256" + case Rs384 => "RS384" + case Rs512 => "RS512" + case Hs256 => "HS256" + case Hs384 => "HS384" + case Hs512 => "HS512" + } + + JsObject( - "alg" -> JsString(jwtHeader.algorithm.name), + "alg" -> JsString(algorithmIdentifier), "typ" -> JsString(jwtHeader.typ) ) } @@ -28,7 +46,17 @@ package object headers { override def read(json: JsValue): JwtHeader = { json.asJsObject.getFields("alg", "typ") match { case Seq(JsString(alg), JsString(typ)) if typ == "JWT" => - Algorithm.forName(alg) match { + + val maybeAlgorithm = alg match { + case "HS256" => Some(Algorithm.Hmac.Hs256) + case "HS384" => Some(Algorithm.Hmac.Hs384) + case "HS512" => Some(Algorithm.Hmac.Hs512) + case "RS256" => Some(Algorithm.Rsa.Rs256) + case "RS384" => Some(Algorithm.Rsa.Rs384) + case "RS512" => Some(Algorithm.Rsa.Rs512) + } + + maybeAlgorithm match { case Some(algorithm) => JwtHeader(algorithm) case _ => throw new InvalidJwtAlgorithmException(s"Unsupported JWT algorithm $alg") } @@ -36,4 +64,4 @@ package object headers { } } } -} // TODO: Many test are probably failing, Fix them! +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala new file mode 100644 index 0000000..f6e6387 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -0,0 +1,155 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.util.CollectionsFactory + +/** Contains the parameter that describes the cryptographic operations and + * parameters employed to JWS Protected Header's and a JWS Payload. + * + * @param protectedHeader set of contained protected Header + * @param unprotectedHeader set of contained unprotected Header + */ +sealed abstract case class JoseHeader private ( + protectedHeaders: Set[JoseHeader.Header.ProtectedHeader], + unprotectedHeaders: Set[JoseHeader.Header.UnprotectedHeader] +) { + + def headers: Set[JoseHeader.Header] = { + protectedHeaders ++ unprotectedHeaders + } +} + +object JoseHeader { + + /** Represents a Header Parameter. + */ + trait Header { + + /** Type of this header. + */ + protected type T + + /** Name fo this header. + * + * @return header name + */ + def name: String + + /** Value of this header. + * + * @return header value + */ + def value: T + } + + /** Headers. + */ + object Header { + + /** JWS protected Header. + */ + trait ProtectedHeader extends Header + + /** Protected headers. + */ + object ProtectedHeader { + + /** Identifies the cryptographic algorithm used to secure the JWS + * + * @param value + */ + final case class Algorithm( + value: algorithms.Algorithm + ) extends ProtectedHeader { + + override def name: String = "alg" + + override type T = algorithms.Algorithm + } + + /** Represents a private protected header that producer and consumer of a + * JWT can agree to use freely unlike registered headers. + * + * @param name name of this header + * @param value value of this header + */ + case class Private[H](name: String, value: H) extends ProtectedHeader { + override type T = H + } + } + + /** JWS unprotected Header. + */ + trait UnprotectedHeader extends Header + + object UnprotectedHeader { + + /** Represents a private unprotected header that producer and consumer of + * a JWT can agree to use freely unlike registered headers. + * + * @param name name of this header + * @param value value of this header + */ + case class Private[H](name: String, value: H) extends UnprotectedHeader { + override type T = H + } + } + } + + /** Constructs a Jose Header for a set of headers. + * + * When multiple headers share the same name the later one in the given list + * of headers remains in the resulting headers list, hereby protected headers + * benefit from a higher precedence than unprotected headers. + * + * @param headers set of headers that should be added + * @return Jose Header containing both the protected and unprotected headers + */ + def apply( + headers: Seq[Header] + ): JoseHeader = { + val (protectedHeaders, unprotectedHeaders) = { + headers.foldLeft( + ( + List.empty[Header.ProtectedHeader], + List.empty[Header.UnprotectedHeader] + ) + ) { case ((protectedHeaders, unprotectedHeaders), header) => + header match { + case header: Header.ProtectedHeader => + (protectedHeaders :+ header, unprotectedHeaders) + case header: Header.UnprotectedHeader => + (protectedHeaders, unprotectedHeaders :+ header) + } + } + } + + val protectedHeadersWithUniqueNames = { + CollectionsFactory.uniqueElements(protectedHeaders)(_.name) + } + + val protectedHeaderNames = { + protectedHeadersWithUniqueNames.map(_.name) + } + + val unprotectedHeadersWithUniqueNames = { + unprotectedHeaders.foldRight(List.empty[Header.UnprotectedHeader]) { + case (header, headers) => + val existsHeaderWithSameName = { + (headers.map(_.name) ++ protectedHeaderNames).contains(header.name) + } + + if (existsHeaderWithSameName) { + headers + } else { + header :: headers + } + } + } + + new JoseHeader( + protectedHeaders = protectedHeadersWithUniqueNames.toSet, + unprotectedHeaders = unprotectedHeadersWithUniqueNames.toSet + ) {} + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala new file mode 100644 index 0000000..0ae7d9c --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala @@ -0,0 +1,5 @@ +package org.janjaali.sprayjwt.jws + +/** A data structure representing a digitally signed or MACed message. + */ +final case class Signature(value: String) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/Claim.scala similarity index 97% rename from spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/Claim.scala index 9d35edd..0e3441e 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/Claim.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/Claim.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.{JsonValue, JsonWriter} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala new file mode 100644 index 0000000..1047114 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala @@ -0,0 +1,30 @@ +package org.janjaali.sprayjwt.jwt + +import org.janjaali.sprayjwt.util.CollectionsFactory + +/** Claims Set represents a data structure whose members are the claims. The + * claim names within a Claims Set MUST be unique. + * + * @param claims uniquely named claims + */ +sealed abstract case class ClaimsSet private (claims: Set[Claim]) + +object ClaimsSet { + + /** Constructs a Claims Set for a set of uniquely named claims. + * + * When multiple claims share the same name the later one in the given list + * of claims remains in the resulting claims set. + * + * @param claims claims that should be added to the Claims Set. + * @return Claims Set + */ + def apply(claims: Seq[Claim]): ClaimsSet = { + + val claimsWithUniqueNames = { + CollectionsFactory.uniqueElements(claims)(_.name) + } + + new ClaimsSet(claimsWithUniqueNames.toSet) {} + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala similarity index 91% rename from spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala index 05dcec1..8e6fb35 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/JwsPayload.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.JsonObject diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/NumericDate.scala similarity index 93% rename from spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala rename to spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/NumericDate.scala index 6f8130c..1b4e540 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/NumericDate.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/NumericDate.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.{CommonJsonWriters, JsonWriter} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala deleted file mode 100644 index 04f023d..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSet.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.janjaali.sprayjwt.jwt.model - -/** Claims Set represents a data structure whose members are the claims. The - * claim names within a Claims Set MUST be unique. - * - * @param claims uniquely named claims - */ -sealed abstract case class ClaimsSet private (claims: Set[Claim]) - -object ClaimsSet { - - /** Constructs a Claim Set for a set of uniquely named claims. - * - * Claims with duplicated claim names override pre-existing claims in the - * same set. - * - * @param claims uniquely named claims - * @return Claim Set - */ - def apply(claims: Seq[Claim]): ClaimsSet = { - - val claimsWithUniqueNames = { - claims.foldRight(List.empty[Claim]) { case (claim, claims) => - if (claims.map(_.name).contains(claim.name)) { - claims - } else { - claim :: claims - } - } - } - - new ClaimsSet(claimsWithUniqueNames.toSet) {} - } -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/util/CollectionsFactory.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/util/CollectionsFactory.scala new file mode 100644 index 0000000..ddc02eb --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/util/CollectionsFactory.scala @@ -0,0 +1,36 @@ +package org.janjaali.sprayjwt.util + +/** Utilities methods to setup collections with given properties. + */ +object CollectionsFactory { + + /** Creates a collection from a given collecting by ensuring that the given + * predicate uniquely matches for at most one element of the returned + * collection. + * + * When multiple elements match the same predicate the later one in the + * sequence remains in the resulting sequence. + * + * Example: + * + *
+    * val origin = List(("a", 1), ("b", 2), ("c", 3), ("b", 4), ("a", 5))
+    * val result = uniqueElements(origin) { elem => elem._1 }
+    *
+    * result // => List((c,3), (b,4), (a,5))
+    * 
+ * + * @param collection + * @param predicate + * @return sequence containing at most one element that matches the predicate + */ + def uniqueElements[T, U](collection: Seq[T])(predicate: T => U): Seq[T] = { + collection.foldRight(List.empty[T]) { case (elem, collection) => + if (collection.map(predicate).contains(predicate(elem))) { + collection + } else { + elem :: collection + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala index bc6c5cc..dc1fcd3 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala @@ -12,7 +12,7 @@ class JwtSpec extends AnyFunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, HS256).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs256).get // scalastyle:off val expectedJwt = @@ -25,7 +25,12 @@ class JwtSpec extends AnyFunSpec { it("encodes as JWT with iss") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" val jwt = LegacyJwt - .encode(payload, secret, HS256, JwtClaims(iss = Some("issuer"))) + .encode( + payload, + secret, + Algorithm.Hmac.Hs256, + JwtClaims(iss = Some("issuer")) + ) .get // scalastyle:off @@ -44,7 +49,7 @@ class JwtSpec extends AnyFunSpec { "admin" -> JsBoolean(true) ) - val jwt = LegacyJwt.encode(jsValue, secret, HS256).get + val jwt = LegacyJwt.encode(jsValue, secret, Algorithm.Hmac.Hs256).get // scalastyle:off val expectedJwt = @@ -63,7 +68,12 @@ class JwtSpec extends AnyFunSpec { ) val jwt = LegacyJwt - .encode(jsValue, secret, HS256, JwtClaims(iss = Some("issuer"))) + .encode( + jsValue, + secret, + Algorithm.Hmac.Hs256, + JwtClaims(iss = Some("issuer")) + ) .get // scalastyle:off @@ -83,7 +93,7 @@ class JwtSpec extends AnyFunSpec { .encode( payload, secret, - HS256, + Algorithm.Hmac.Hs256, JwtClaims( iss = Some("issuer"), sub = Some("subject"), @@ -155,7 +165,7 @@ class JwtSpec extends AnyFunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, HS384).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs384).get // scalastyle:off val expectedJwt = @@ -183,7 +193,7 @@ class JwtSpec extends AnyFunSpec { it("encodes as JWT") { val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, HS512).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs512).get // scalastyle:off val expectedJwt = @@ -215,7 +225,7 @@ class JwtSpec extends AnyFunSpec { finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, RS256).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs256).get // scalastyle:off val expectedJwt = @@ -253,7 +263,7 @@ class JwtSpec extends AnyFunSpec { finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, RS384).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs384).get // scalastyle:off val expectedJwt = @@ -291,7 +301,7 @@ class JwtSpec extends AnyFunSpec { finally source.close val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, RS512).get + val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs512).get // scalastyle:off val expectedJwt = diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala new file mode 100644 index 0000000..956be3a --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala @@ -0,0 +1,8 @@ +package org.janjaali.sprayjwt.algorithms + +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +class AlgorithmSpec extends ScalaTestSpec { + + // TODO: Add some tests +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala deleted file mode 100644 index 677e8ca..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/HashingAlgorithmSpec.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -import org.scalatest.funspec.AnyFunSpec - -class HashingAlgorithmSpec extends AnyFunSpec { - - describe("HashingAlgorithm") { - describe("maps Strings to HashingAlgorithm") { - it("maps HS256") { - Algorithm("HS256") match { - case Some(alg) => assert(alg == HS256) - case _ => fail - } - } - - it("maps HS384") { - Algorithm("HS384") match { - case Some(alg) => assert(alg == HS384) - case _ => fail - } - } - - it("maps HS512") { - Algorithm("HS512") match { - case Some(alg) => assert(alg == HS512) - case _ => fail - } - } - } - } - -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala new file mode 100644 index 0000000..2aa9027 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala @@ -0,0 +1,29 @@ +package org.janjaali.sprayjwt.algorithms + +import org.scalacheck.Gen + +object ScalaCheckGenerators { + + def rsaAlgorithmGen: Gen[Algorithm.Rsa] = { + Gen.oneOf( + Algorithm.Rsa.Rs256, + Algorithm.Rsa.Rs384, + Algorithm.Rsa.Rs512 + ) + } + + def hmacAlgorithmGen: Gen[Algorithm.Hmac] = { + Gen.oneOf( + Algorithm.Hmac.Hs256, + Algorithm.Hmac.Hs384, + Algorithm.Hmac.Hs512 + ) + } + + def algorithmGen: Gen[Algorithm] = { + Gen.oneOf( + hmacAlgorithmGen, + rsaAlgorithmGen + ) + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala index 763016c..5b73a82 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala @@ -1,18 +1,20 @@ package org.janjaali.sprayjwt.headers -import org.janjaali.sprayjwt.algorithms.HS256 import org.scalatest.funspec.AnyFunSpec import spray.json._ +import org.janjaali.sprayjwt.algorithms.Algorithm class JwtHeaderJsonProtocolSpec extends AnyFunSpec { describe("JwtHeaderJsonProtocol trait") { it("converts JWT-Header to JsValue") { - val jwtHeaderJson = JwtHeader(HS256).toJson - assert(jwtHeaderJson == JsObject( - "typ" -> JsString("JWT"), - "alg" -> JsString("HS256") - )) + val jwtHeaderJson = JwtHeader(Algorithm.Hmac.Hs256).toJson + assert( + jwtHeaderJson == JsObject( + "typ" -> JsString("JWT"), + "alg" -> JsString("HS256") + ) + ) } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala new file mode 100644 index 0000000..3a1a4cd --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala @@ -0,0 +1,101 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.algorithms.Algorithm +import org.janjaali.sprayjwt.tests.{ScalaCheckGeneratorsSampler, ScalaTestSpec} +import org.scalacheck.Gen +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "Jose Header" - { + + "when constructed" - { + + val sut = JoseHeader.apply _ + + "should not contain headers with the same name." in { + + forAll(ScalaCheckGenerators.headersGen) { headers => + val headerNames = sut(headers).headers.map(_.name) + + headerNames.toSet.size shouldBe headerNames.size + } + } + + "should add all headers with unique names." in { + + forAll(ScalaCheckGenerators.headersGen) { headers => + val distinctNamedHeaders = { + headers.groupBy(_.name).values.map(_.head).toSeq + } + + val joseHeader = sut(distinctNamedHeaders) + + joseHeader.headers should contain theSameElementsAs { + distinctNamedHeaders + } + } + } + + "with unprotected headers with the same name" - { + + "should add the later ones." in { + + val joseHeaders = sut( + List( + JoseHeader.Header.UnprotectedHeader.Private("name", 1), + JoseHeader.Header.UnprotectedHeader.Private("name", 3) + ) + ) + + joseHeaders.headers should contain only { + JoseHeader.Header.UnprotectedHeader.Private("name", 3) + } + } + } + + "with protected headers with the same name" - { + + "should add the later ones." in { + + val joseHeaders = sut( + List( + JoseHeader.Header.ProtectedHeader.Private("name", 1), + JoseHeader.Header.ProtectedHeader.Private("name", 3) + ) + ) + + joseHeaders.headers should contain only { + JoseHeader.Header.ProtectedHeader.Private("name", 3) + } + } + } + + "with protected and unprotected headers with the same name" - { + + "should add the protected ones." in { + + val joseHeaders = sut( + List( + JoseHeader.Header.ProtectedHeader.Private("name", 1), + JoseHeader.Header.UnprotectedHeader.Private("name", 3) + ) + ) + + joseHeaders.headers should contain only { + JoseHeader.Header.ProtectedHeader.Private("name", 1) + } + } + } + } + + "should return all headers." in { + + forAll(ScalaCheckGenerators.joseHeader) { joseHeader => + joseHeader.headers should contain theSameElementsAs { + joseHeader.protectedHeaders ++ joseHeader.unprotectedHeaders + } + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala new file mode 100644 index 0000000..31e0f88 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala @@ -0,0 +1,64 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.algorithms +import org.scalacheck.{Arbitrary, Gen} + +object ScalaCheckGenerators { + + def algorithmHeaderGen: Gen[JoseHeader.Header.ProtectedHeader.Algorithm] = { + + import JoseHeader.Header.ProtectedHeader.Algorithm + + algorithms.ScalaCheckGenerators.algorithmGen.map(Algorithm.apply) + } + + def privateProtectedHeaderGen[T: Arbitrary]: Gen[ + JoseHeader.Header.ProtectedHeader.Private[T] + ] = { + + val valueGen = implicitly[Arbitrary[T]].arbitrary + + for { + name <- Gen.alphaStr + value <- valueGen + } yield JoseHeader.Header.ProtectedHeader.Private(name, value) + } + + def privateUnprotectedHeaderGen[T: Arbitrary]: Gen[ + JoseHeader.Header.UnprotectedHeader.Private[T] + ] = { + + val valueGen = implicitly[Arbitrary[T]].arbitrary + + for { + name <- Gen.alphaStr + value <- valueGen + } yield JoseHeader.Header.UnprotectedHeader.Private(name, value) + } + + def protectedHeaderGen: Gen[JoseHeader.Header.ProtectedHeader] = { + Gen.oneOf( + privateProtectedHeaderGen[String], + algorithmHeaderGen + ) + } + + def unprotectedHeaderGen: Gen[JoseHeader.Header.UnprotectedHeader] = { + privateUnprotectedHeaderGen[String] + } + + def headerGen: Gen[JoseHeader.Header] = { + Gen.oneOf( + protectedHeaderGen, + unprotectedHeaderGen + ) + } + + def headersGen: Gen[List[JoseHeader.Header]] = { + Gen.listOf(headerGen) + } + + def joseHeader: Gen[JoseHeader] = { + headersGen.map(JoseHeader.apply) + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimSpec.scala similarity index 92% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimSpec.scala index 4c064ff..3c9c56a 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimSpec.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.JsonNumber import org.janjaali.sprayjwt.tests.ScalaTestSpec diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala new file mode 100644 index 0000000..ad2d3a6 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala @@ -0,0 +1,57 @@ +package org.janjaali.sprayjwt.jwt + +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ +import org.scalacheck.Gen +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class ClaimsSetSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "Claim Set" - { + + "when constructed" - { + + val sut = ClaimsSet.apply _ + + "should not contain claims with the same name." in { + + forAll(ScalaCheckGenerators.claimsGen) { claims => + val headerNames = sut(claims).claims.map(_.name) + + headerNames.toSet.size shouldBe headerNames.size + } + } + + "should add all claims with uniqueNames." in { + + forAll(ScalaCheckGenerators.claimsGen) { claims => + val distinctNamedClaims = { + claims.groupBy(_.name).values.map(_.head).toSeq + } + + val claimsSet = sut(distinctNamedClaims) + + claimsSet.claims should contain theSameElementsAs { + distinctNamedClaims + } + } + } + + "with claims with the same name" - { + + "should add the later ones." in { + val claimsSet = sut( + List( + Claim.Private("name", 1), + Claim.Private("name", 4) + ) + ) + + claimsSet.claims should contain only { + Claim.Private("name", 4) + } + } + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala similarity index 94% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala index 70f5ffa..9aab75a 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/JwsPayloadSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.tests.ScalaTestSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/NumericDateSpec.scala similarity index 93% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/NumericDateSpec.scala index 894daab..5b12d7e 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/NumericDateSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/NumericDateSpec.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.JsonNumber import org.janjaali.sprayjwt.tests.ScalaTestSpec diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala similarity index 91% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala index 4632a99..502985e 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt.model +package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.json.CommonJsonWriters import org.scalacheck.Gen @@ -55,8 +55,12 @@ private[jwt] object ScalaCheckGenerators { ) } + def claimsGen: Gen[List[Claim]] = { + Gen.listOf(claimGen) + } + def claimsSetGen: Gen[ClaimsSet] = { - Gen.listOf(claimGen).map(ClaimsSet.apply) + claimsGen.map(ClaimsSet.apply) } def jwsPayloadGen: Gen[JwsPayload] = { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala deleted file mode 100644 index 710f08d..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/model/ClaimsSetSpec.scala +++ /dev/null @@ -1,47 +0,0 @@ -package org.janjaali.sprayjwt.jwt.model - -import org.janjaali.sprayjwt.tests.ScalaTestSpec -import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ -import org.scalacheck.Gen -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -class ClaimsSetSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { - - "Claim Set" - { - - "when constructed with claims that do not share same claim names" - { - - "should add all claims." in { - - val claimsWithDistinctNames = { - Gen - .listOf(ScalaCheckGenerators.claimGen) - .map(_.groupBy(_.name)) - .map(_.mapValues(_.headOption)) - .map(_.values.flatten) - .map(_.toSeq) - } - - forAll(claimsWithDistinctNames) { claims => - ClaimsSet(claims).claims should contain theSameElementsAs claims - } - } - } - - "when constructed with claims that share same claim names" - { - - val claim1 = Claim.Private("name", 1) - val claim2 = Claim.Private("name", 2) - - val claims = List( - claim1, - claim2 - ) - - "should only add the later ones." in { - - ClaimsSet(claims).claims should contain only claim2 - } - } - } -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala new file mode 100644 index 0000000..8bbeba3 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala @@ -0,0 +1,21 @@ +package org.janjaali.sprayjwt.tests + +import org.scalacheck.Gen + +/** Provides sampler based on ScalaCheck generators and enables to do the + * following: + * + *
+  * val gen: Gen[T] = ???
+  * val t: T = gen.get
+  * 
+ */ +trait ScalaCheckGeneratorsSampler { + + implicit class Sampler[T](gen: Gen[T]) { + + def get: T = { + gen.sample.get + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/util/CollectionsFactorySpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/util/CollectionsFactorySpec.scala new file mode 100644 index 0000000..2fb1330 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/util/CollectionsFactorySpec.scala @@ -0,0 +1,38 @@ +package org.janjaali.sprayjwt.util + +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +class CollectionsFactorySpec extends ScalaTestSpec { + + "CollectionsFactory" - { + + val sut = CollectionsFactory + + "should create collections containing at most one element for a given predicate" - { + + "when empty should be empty" in { + + sut.uniqueElements(Nil)(_ => true) shouldBe Nil + } + + "when predicate matches all should contain last matching element." in { + + sut.uniqueElements(List(1, 2, 3))(_ < 10) shouldBe List(3) + } + + "when predicate matches none should contain last none matching element." in { + + sut.uniqueElements(List(1, 2, 3))(_ > 10) shouldBe List(3) + } + + "when predicate matches multiple elements should contain last matching element." in { + + val pairs = List(("a", 1), ("b", 2), ("c", 3), ("b", 4), ("a", 5)) + + sut.uniqueElements(pairs)(elem => elem._1) shouldBe { + List(("c", 3), ("b", 4), ("a", 5)) + } + } + } + } +} From 68a0cc61c9136f43832edcc776915c733d3572be Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Wed, 24 Mar 2021 22:45:12 +0100 Subject: [PATCH 24/43] WIP. --- .../scala/org/janjaali/sprayjwt/Jwt.scala | 6 ++--- .../janjaali/sprayjwt/jws/JwsPayload.scala | 23 +++++++++++++++++++ .../janjaali/sprayjwt/jws/JwsSignature.scala | 5 ++++ .../org/janjaali/sprayjwt/jws/Signature.scala | 5 ---- .../org/janjaali/sprayjwt/jwt/ClaimsSet.scala | 15 ++++++------ .../janjaali/sprayjwt/jwt/JwsPayload.scala | 22 ------------------ .../{jwt => jws}/JwsPayloadSpec.scala | 4 ++-- .../sprayjwt/jws/ScalaCheckGenerators.scala | 6 ++++- .../janjaali/sprayjwt/jwt/ClaimsSetSpec.scala | 2 +- .../sprayjwt/jwt/ScalaCheckGenerators.scala | 10 +++----- 10 files changed, 50 insertions(+), 48 deletions(-) create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/{jwt => jws}/JwsPayloadSpec.scala (87%) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala index b2c8994..a674470 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala @@ -15,8 +15,8 @@ import spray.json._ import scala.util.{Success, Try} import org.janjaali.sprayjwt.json._ -import org.janjaali.sprayjwt.jwt.ClaimsSet -import org.janjaali.sprayjwt.jwt.JwsPayload +import org.janjaali.sprayjwt.jwt.JwtClaimsSet +import org.janjaali.sprayjwt.jws.JwsPayload /** TODO: */ @@ -88,7 +88,7 @@ private case class JoseHeader(algorithm: Algorithm) { object Jwt { def apply( - claims: ClaimsSet, + claims: JwtClaimsSet, algorithm: Algorithm, secret: String )(implicit jsonStringSerializer: JsonStringSerializer): Jwt = { diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala new file mode 100644 index 0000000..99caed9 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala @@ -0,0 +1,23 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.json.JsonObject +import org.janjaali.sprayjwt.jwt.JwtClaimsSet + +/** Payload that consists of the Claims Set that has to be secured. + * + * @param claimsSet claims set that has to be secured + */ +final case class JwsPayload(claimsSet: JwtClaimsSet) { + + /** JSON representation of this JWS payload. + * + * @return JSON object + */ + def asJson: JsonObject = { + JsonObject( + claimsSet.claims.map { claim => + claim.name -> claim.valueAsJson + }.toMap + ) + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala new file mode 100644 index 0000000..9b658a1 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala @@ -0,0 +1,5 @@ +package org.janjaali.sprayjwt.jws + +/** Digital signature or MAC over the JWS Protected Header and the JWS Payload. + */ +final case class JWsSignature(value: String) extends AnyVal diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala deleted file mode 100644 index 0ae7d9c..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Signature.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.janjaali.sprayjwt.jws - -/** A data structure representing a digitally signed or MACed message. - */ -final case class Signature(value: String) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala index 1047114..2a75454 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/ClaimsSet.scala @@ -2,14 +2,15 @@ package org.janjaali.sprayjwt.jwt import org.janjaali.sprayjwt.util.CollectionsFactory -/** Claims Set represents a data structure whose members are the claims. The - * claim names within a Claims Set MUST be unique. +/** Claims Set represents a data structure whose members are the claims. * - * @param claims uniquely named claims + * The claim names within a Claims Set are unique. + * + * @param claims set of uniquely named claims */ -sealed abstract case class ClaimsSet private (claims: Set[Claim]) +sealed abstract case class JwtClaimsSet private (claims: Set[Claim]) -object ClaimsSet { +object JwtClaimsSet { /** Constructs a Claims Set for a set of uniquely named claims. * @@ -19,12 +20,12 @@ object ClaimsSet { * @param claims claims that should be added to the Claims Set. * @return Claims Set */ - def apply(claims: Seq[Claim]): ClaimsSet = { + def apply(claims: Seq[Claim]): JwtClaimsSet = { val claimsWithUniqueNames = { CollectionsFactory.uniqueElements(claims)(_.name) } - new ClaimsSet(claimsWithUniqueNames.toSet) {} + new JwtClaimsSet(claimsWithUniqueNames.toSet) {} } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala deleted file mode 100644 index 8e6fb35..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jwt/JwsPayload.scala +++ /dev/null @@ -1,22 +0,0 @@ -package org.janjaali.sprayjwt.jwt - -import org.janjaali.sprayjwt.json.JsonObject - -/** JWS Payload consisting of a Claims Set. - * - * @param claims claims of this JWS Payload - */ -final case class JwsPayload(claims: ClaimsSet) { - - /** JSON representation of this JWS payload. - * - * @return JSON object - */ - def asJson: JsonObject = { - JsonObject( - claims.claims.map { claim => - claim.name -> claim.valueAsJson - }.toMap - ) - } -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JwsPayloadSpec.scala similarity index 87% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JwsPayloadSpec.scala index 9aab75a..ca892d0 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/JwsPayloadSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JwsPayloadSpec.scala @@ -1,4 +1,4 @@ -package org.janjaali.sprayjwt.jwt +package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.tests.ScalaTestSpec import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -14,7 +14,7 @@ class JwsPayloadSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { val expectedJsonObjectMembers = { - jwsPayload.claims.claims.map { claim => + jwsPayload.claimsSet.claims.map { claim => claim.name -> claim.valueAsJson }.toMap } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala index 31e0f88..29eefe7 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala @@ -1,6 +1,6 @@ package org.janjaali.sprayjwt.jws -import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.{algorithms, jwt} import org.scalacheck.{Arbitrary, Gen} object ScalaCheckGenerators { @@ -61,4 +61,8 @@ object ScalaCheckGenerators { def joseHeader: Gen[JoseHeader] = { headersGen.map(JoseHeader.apply) } + + def jwsPayloadGen: Gen[JwsPayload] = { + jwt.ScalaCheckGenerators.claimsSetGen.map(JwsPayload.apply) + } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala index ad2d3a6..caabc0c 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ClaimsSetSpec.scala @@ -11,7 +11,7 @@ class ClaimsSetSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { "when constructed" - { - val sut = ClaimsSet.apply _ + val sut = JwtClaimsSet.apply _ "should not contain claims with the same name." in { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala index 502985e..ac0a255 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala @@ -5,7 +5,7 @@ import org.scalacheck.Gen import java.time.Instant -private[jwt] object ScalaCheckGenerators { +object ScalaCheckGenerators { /** Generator for numeric dates containing epoch seconds in the time frame of: * [now - 100 years, now + 100 years]. @@ -59,11 +59,7 @@ private[jwt] object ScalaCheckGenerators { Gen.listOf(claimGen) } - def claimsSetGen: Gen[ClaimsSet] = { - claimsGen.map(ClaimsSet.apply) - } - - def jwsPayloadGen: Gen[JwsPayload] = { - claimsSetGen.map(JwsPayload.apply) + def claimsSetGen: Gen[JwtClaimsSet] = { + claimsGen.map(JwtClaimsSet.apply) } } From 6c2b6b64cc5a6f607c904a688dcbb1c8397821d4 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Wed, 7 Apr 2021 19:13:26 +0200 Subject: [PATCH 25/43] WIP --- .../sprayjwt/algorithms/Algorithm.scala | 41 ++++- .../janjaali/sprayjwt/algorithms/Secret.scala | 10 ++ .../sprayjwt/algorithms/dan.worksheet.sc | 11 ++ .../sprayjwt/encoder/Base64Encoder.scala | 2 +- .../org/janjaali/sprayjwt/jws/Header.scala | 125 +++++++++++++++ .../janjaali/sprayjwt/jws/JoseHeader.scala | 142 ++---------------- .../janjaali/sprayjwt/jws/JwsSignature.scala | 2 +- .../janjaali/sprayjwt/jws/HeaderSpec.scala | 34 +++++ .../sprayjwt/jws/JoseHeaderSpec.scala | 54 ++----- .../sprayjwt/jws/ScalaCheckGenerators.scala | 44 ++---- 10 files changed, 262 insertions(+), 203 deletions(-) create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index e353752..cf40327 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -6,6 +6,7 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} +import org.janjaali.sprayjwt.jws.{JoseHeader, JwsPayload, JwsSignature} import java.io.{IOException, StringReader} import java.security.{PrivateKey, PublicKey, Signature} @@ -14,9 +15,18 @@ import java.security.{PrivateKey, PublicKey, Signature} */ sealed trait Algorithm { - // TODO: Maybe a better suited signature - // Digitally signs the contents of the given JWS Header and the JWS Payload. - // def sign(header: JoseHeader, payload: JwsPayload): String + /** Digitally signs the protected headers of the given Jose Header and the Jws + * Payload. + * + * @param joseHeader jose header + * @param jwsPayload jws payload + * @param secret secret + */ + def sign( + joseHeader: JoseHeader, + jwsPayload: JwsPayload, + secret: Secret + ): JwsSignature /** Signs data. // TODO: legacy? * @@ -52,6 +62,22 @@ object Algorithm { protected def hashingAlgorithmName: String + override def sign( + joseHeader: JoseHeader, + jwsPayload: JwsPayload, + secret: Secret + ): JwsSignature = { + + val key = new SecretKeySpec(secret.asByteArray, hashingAlgorithmName) + + val mac = Mac.getInstance(hashingAlgorithmName, provider) + mac.init(key) + + val signature = mac.doFinal(???) + + JwsSignature(Base64Encoder.encode(signature)) + } + // TODO: Check implementation override def sign(data: String, secret: String): String = { @@ -102,6 +128,15 @@ object Algorithm { protected def hashingAlgorithmName: String + override def sign( + joseHeader: JoseHeader, + jwsPayload: JwsPayload, + secret: Secret + ): JwsSignature = { + + ??? + } + // TODO: Check implementation override def sign(data: String, secret: String): String = { diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala new file mode 100644 index 0000000..8d795a5 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala @@ -0,0 +1,10 @@ +package org.janjaali.sprayjwt.algorithms + +/** Represents a String based secret value. + */ +final case class Secret(value: String) extends AnyVal { + + def asByteArray: Array[Byte] = { + value.getBytes("UTF-8") + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc new file mode 100644 index 0000000..8cc0227 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc @@ -0,0 +1,11 @@ +val encoder = java.util.Base64.getEncoder() + +val urlEncoder = java.util.Base64.getUrlEncoder() + +val json = """{"typ":"JWT","alg":"HS256"}""" + +val jsonBytes = json.getBytes("UTF-8") + +val encodedBytes = urlEncoder.encode(jsonBytes) + +new String(encodedBytes) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala index cfc82cc..b8479af 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala @@ -15,7 +15,7 @@ private[sprayjwt] object Base64Encoder { * @return Base64 encoded String */ def encode(text: String): String = { - val textAsByteArray = ByteEncoder.getBytes(text) + val textAsByteArray = text.getBytes("UTF-8") encode(textAsByteArray) } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala new file mode 100644 index 0000000..c88d8c4 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala @@ -0,0 +1,125 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.json.{JsonString, JsonValue, JsonWriter} + +/** Represents a Header. + */ +trait Header { + + /** Type of this header. + */ + protected type T + + /** Json writer for this header's value. + */ + protected def valueJsonWriter: JsonWriter[T] + + /** Name fo this header. + * + * @return header name + */ + def name: String + + /** Value of this header. + * + * @return header value + */ + def value: T + + /** JSON representation of this header's value. + * + * @return JSON value + */ + private[sprayjwt] def valueAsJson: JsonValue = { + valueJsonWriter.write(this.value) + } +} + +/** Provide headers. + */ +object Header { + + /** Identifies the cryptographic algorithm used to secure the JWS + * + * @param value algorithm + */ + final case class Algorithm( + value: algorithms.Algorithm + ) extends Header { + + override def name: String = "alg" + + override type T = algorithms.Algorithm + + override protected def valueJsonWriter: JsonWriter[ + algorithms.Algorithm + ] = { + new JsonWriter[algorithms.Algorithm] { + + override def write(algorithm: algorithms.Algorithm): JsonValue = { + algorithm match { + case algorithms.Algorithm.Rsa.Rs256 => JsonString("RS256") + case algorithms.Algorithm.Rsa.Rs384 => JsonString("RS384") + case algorithms.Algorithm.Rsa.Rs512 => JsonString("RS512") + case algorithms.Algorithm.Hmac.Hs256 => JsonString("HS256") + case algorithms.Algorithm.Hmac.Hs384 => JsonString("HS384") + case algorithms.Algorithm.Hmac.Hs512 => JsonString("HS512") + } + } + } + } + } + + /** Declares the media type of the complete JWS. + * + * @param value type + */ + final case class Type( + value: Type.Value + ) extends Header { + + override def name: String = "typ" + + override type T = Type.Value + + override protected def valueJsonWriter: JsonWriter[Type.Value] = { + new JsonWriter[Type.Value] { + + override def write(value: Type.Value): JsonValue = { + value match { + case Type.Value.Jwt => JsonString("JWT") + } + } + } + } + } + + /** Provides type values. + */ + object Type { + + sealed trait Value + + object Value { + + /** Type value JWT. + */ + case object Jwt extends Value + } + } + + /** Represents a private header that producer and consumer of a JWT can + * agree to use freely unlike registered headers. + * + * @param name name of this header + * @param value value of this header + */ + case class Private[H: JsonWriter](name: String, value: H) extends Header { + override type T = H + + override protected def valueJsonWriter: JsonWriter[H] = { + implicitly[JsonWriter[T]] + } + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala index f6e6387..12dc415 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -1,155 +1,43 @@ package org.janjaali.sprayjwt.jws -import org.janjaali.sprayjwt.algorithms import org.janjaali.sprayjwt.util.CollectionsFactory +import org.janjaali.sprayjwt.json.JsonObject -/** Contains the parameter that describes the cryptographic operations and +/** Javascript Object Signing and Encryption (JOSE Header) that contains + * the parameter that describes the cryptographic operations and * parameters employed to JWS Protected Header's and a JWS Payload. * - * @param protectedHeader set of contained protected Header - * @param unprotectedHeader set of contained unprotected Header + * @param headers set of contained headers */ sealed abstract case class JoseHeader private ( - protectedHeaders: Set[JoseHeader.Header.ProtectedHeader], - unprotectedHeaders: Set[JoseHeader.Header.UnprotectedHeader] + headers: Set[Header] ) { - def headers: Set[JoseHeader.Header] = { - protectedHeaders ++ unprotectedHeaders + def asJson: JsonObject = { + JsonObject(headers.map { header => + header.name -> header.valueAsJson + }.toMap) } } object JoseHeader { - /** Represents a Header Parameter. - */ - trait Header { - - /** Type of this header. - */ - protected type T - - /** Name fo this header. - * - * @return header name - */ - def name: String - - /** Value of this header. - * - * @return header value - */ - def value: T - } - - /** Headers. - */ - object Header { - - /** JWS protected Header. - */ - trait ProtectedHeader extends Header - - /** Protected headers. - */ - object ProtectedHeader { - - /** Identifies the cryptographic algorithm used to secure the JWS - * - * @param value - */ - final case class Algorithm( - value: algorithms.Algorithm - ) extends ProtectedHeader { - - override def name: String = "alg" - - override type T = algorithms.Algorithm - } - - /** Represents a private protected header that producer and consumer of a - * JWT can agree to use freely unlike registered headers. - * - * @param name name of this header - * @param value value of this header - */ - case class Private[H](name: String, value: H) extends ProtectedHeader { - override type T = H - } - } - - /** JWS unprotected Header. - */ - trait UnprotectedHeader extends Header - - object UnprotectedHeader { - - /** Represents a private unprotected header that producer and consumer of - * a JWT can agree to use freely unlike registered headers. - * - * @param name name of this header - * @param value value of this header - */ - case class Private[H](name: String, value: H) extends UnprotectedHeader { - override type T = H - } - } - } - - /** Constructs a Jose Header for a set of headers. + /** Constructs a JOSE Header for a sequence of headers. * * When multiple headers share the same name the later one in the given list - * of headers remains in the resulting headers list, hereby protected headers - * benefit from a higher precedence than unprotected headers. + * of headers remains in the resulting headers list. * * @param headers set of headers that should be added - * @return Jose Header containing both the protected and unprotected headers + * @return JOSE Header */ def apply( headers: Seq[Header] ): JoseHeader = { - val (protectedHeaders, unprotectedHeaders) = { - headers.foldLeft( - ( - List.empty[Header.ProtectedHeader], - List.empty[Header.UnprotectedHeader] - ) - ) { case ((protectedHeaders, unprotectedHeaders), header) => - header match { - case header: Header.ProtectedHeader => - (protectedHeaders :+ header, unprotectedHeaders) - case header: Header.UnprotectedHeader => - (protectedHeaders, unprotectedHeaders :+ header) - } - } - } - - val protectedHeadersWithUniqueNames = { - CollectionsFactory.uniqueElements(protectedHeaders)(_.name) - } - - val protectedHeaderNames = { - protectedHeadersWithUniqueNames.map(_.name) - } - - val unprotectedHeadersWithUniqueNames = { - unprotectedHeaders.foldRight(List.empty[Header.UnprotectedHeader]) { - case (header, headers) => - val existsHeaderWithSameName = { - (headers.map(_.name) ++ protectedHeaderNames).contains(header.name) - } - if (existsHeaderWithSameName) { - headers - } else { - header :: headers - } - } + val uniquelyNamedHeaders = { + CollectionsFactory.uniqueElements(headers)(_.name) } - new JoseHeader( - protectedHeaders = protectedHeadersWithUniqueNames.toSet, - unprotectedHeaders = unprotectedHeadersWithUniqueNames.toSet - ) {} + new JoseHeader(uniquelyNamedHeaders.toSet) {} } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala index 9b658a1..5f462dc 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsSignature.scala @@ -2,4 +2,4 @@ package org.janjaali.sprayjwt.jws /** Digital signature or MAC over the JWS Protected Header and the JWS Payload. */ -final case class JWsSignature(value: String) extends AnyVal +final case class JwsSignature(value: String) extends AnyVal diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala new file mode 100644 index 0000000..ae7c8b2 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala @@ -0,0 +1,34 @@ +package org.janjaali.sprayjwt.jws + +import org.janjaali.sprayjwt.json.JsonString +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks + +class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { + + "Algorithm header" - { + + "when serialized as JSON value" - { + + "should result in a JSON string." in { + + forAll(ScalaCheckGenerators.algorithmHeaderGen) { header => + header.valueAsJson shouldBe a[JsonString] + } + } + } + } + + "Type header" - { + + "when serialized as JSON value" - { + + "should result in a JSON string." in { + + forAll(ScalaCheckGenerators.typeHeaderGen) { header => + header.valueAsJson shouldBe a[JsonString] + } + } + } + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala index 3a1a4cd..7b0d6c5 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala @@ -1,6 +1,7 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.algorithms.Algorithm +import org.janjaali.sprayjwt.json.JsonObject import org.janjaali.sprayjwt.tests.{ScalaCheckGeneratorsSampler, ScalaTestSpec} import org.scalacheck.Gen import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -37,63 +38,32 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { } } - "with unprotected headers with the same name" - { + "with headers with the same name" - { "should add the later ones." in { - val joseHeaders = sut( - List( - JoseHeader.Header.UnprotectedHeader.Private("name", 1), - JoseHeader.Header.UnprotectedHeader.Private("name", 3) - ) - ) - - joseHeaders.headers should contain only { - JoseHeader.Header.UnprotectedHeader.Private("name", 3) - } - } - } - - "with protected headers with the same name" - { - - "should add the later ones." in { + import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ val joseHeaders = sut( List( - JoseHeader.Header.ProtectedHeader.Private("name", 1), - JoseHeader.Header.ProtectedHeader.Private("name", 3) + Header.Private("name", 1), + Header.Private("name", 3) ) ) - joseHeaders.headers should contain only { - JoseHeader.Header.ProtectedHeader.Private("name", 3) - } + joseHeaders.headers should contain only Header.Private("name", 3) } } + } - "with protected and unprotected headers with the same name" - { - - "should add the protected ones." in { + "when serialized as JSON" - { - val joseHeaders = sut( - List( - JoseHeader.Header.ProtectedHeader.Private("name", 1), - JoseHeader.Header.UnprotectedHeader.Private("name", 3) - ) - ) - - joseHeaders.headers should contain only { - JoseHeader.Header.ProtectedHeader.Private("name", 1) - } - } - } - } + "should result in a JSON object." in { - "should return all headers." in { + forAll(ScalaCheckGenerators.joseHeader) { joseHeader => + joseHeader.asJson shouldBe a[JsonObject] - forAll(ScalaCheckGenerators.joseHeader) { joseHeader => - joseHeader.headers should contain theSameElementsAs { - joseHeader.protectedHeaders ++ joseHeader.unprotectedHeaders + fail("add some more tests for this method.") } } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala index 29eefe7..8c10222 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/ScalaCheckGenerators.scala @@ -2,30 +2,23 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.{algorithms, jwt} import org.scalacheck.{Arbitrary, Gen} +import org.janjaali.sprayjwt.json.JsonWriter object ScalaCheckGenerators { - def algorithmHeaderGen: Gen[JoseHeader.Header.ProtectedHeader.Algorithm] = { - - import JoseHeader.Header.ProtectedHeader.Algorithm - - algorithms.ScalaCheckGenerators.algorithmGen.map(Algorithm.apply) + def algorithmHeaderGen: Gen[Header.Algorithm] = { + algorithms.ScalaCheckGenerators.algorithmGen.map(Header.Algorithm.apply) } - def privateProtectedHeaderGen[T: Arbitrary]: Gen[ - JoseHeader.Header.ProtectedHeader.Private[T] - ] = { + def typeHeaderGen: Gen[Header.Type] = { - val valueGen = implicitly[Arbitrary[T]].arbitrary + import Header.Type - for { - name <- Gen.alphaStr - value <- valueGen - } yield JoseHeader.Header.ProtectedHeader.Private(name, value) + Type(Type.Value.Jwt) } - def privateUnprotectedHeaderGen[T: Arbitrary]: Gen[ - JoseHeader.Header.UnprotectedHeader.Private[T] + def privateHeaderGen[T: JsonWriter: Arbitrary]: Gen[ + Header.Private[T] ] = { val valueGen = implicitly[Arbitrary[T]].arbitrary @@ -33,28 +26,21 @@ object ScalaCheckGenerators { for { name <- Gen.alphaStr value <- valueGen - } yield JoseHeader.Header.UnprotectedHeader.Private(name, value) + } yield Header.Private(name, value) } - def protectedHeaderGen: Gen[JoseHeader.Header.ProtectedHeader] = { - Gen.oneOf( - privateProtectedHeaderGen[String], - algorithmHeaderGen - ) - } + def headerGen: Gen[Header] = { - def unprotectedHeaderGen: Gen[JoseHeader.Header.UnprotectedHeader] = { - privateUnprotectedHeaderGen[String] - } + import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ - def headerGen: Gen[JoseHeader.Header] = { Gen.oneOf( - protectedHeaderGen, - unprotectedHeaderGen + algorithmHeaderGen, + typeHeaderGen, + privateHeaderGen[String] ) } - def headersGen: Gen[List[JoseHeader.Header]] = { + def headersGen: Gen[List[Header]] = { Gen.listOf(headerGen) } From 0621f1e0410662ed5ba1027b01edfaead1207e82 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sat, 10 Apr 2021 17:35:52 +0200 Subject: [PATCH 26/43] Update SBT version to v1.5.0. --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index ed145bb..b366316 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.4.5 +sbt.version = 1.5.0 From 4a6b7081610fc2dc3e51e8bfb799c539f067078e Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sat, 10 Apr 2021 17:50:09 +0200 Subject: [PATCH 27/43] Udpate scalafmt version to v2.7.5. --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index ffbdff9..010fa6d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1 +1 @@ -version = "2.7.4" +version = "2.7.5" From 6876cca8ee66a6a3e510aaf26d991a4f80fe2e3c Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sun, 11 Apr 2021 17:17:32 +0200 Subject: [PATCH 28/43] WIP --- .scalafmt.conf | 3 +++ .../test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 010fa6d..332ae8f 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1 +1,4 @@ version = "2.7.5" + +newlines.topLevelStatements = [before,after] +newlines.topLevelStatementsMinBreaks = 5 diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala index 7b0d6c5..1e399b8 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala @@ -23,7 +23,7 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { } } - "should add all headers with unique names." in { + "should add headers with distinct names." in { forAll(ScalaCheckGenerators.headersGen) { headers => val distinctNamedHeaders = { From 2de49f280d33256faff4c8431c70edca907c0f3b Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 13 Apr 2021 22:00:48 +0200 Subject: [PATCH 29/43] WIP. --- .scalafmt.conf | 4 ++-- .../org/janjaali/sprayjwt/json/JsonValue.scala | 2 +- .../org/janjaali/sprayjwt/jws/JoseHeader.scala | 4 +--- .../org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala | 14 ++++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 332ae8f..da3b0d4 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ version = "2.7.5" -newlines.topLevelStatements = [before,after] -newlines.topLevelStatementsMinBreaks = 5 +newlines.topLevelStatements = [before] +newlines.afterCurlyLambdaParams = preserve diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 3ad77f2..9e99cab 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -2,7 +2,7 @@ package org.janjaali.sprayjwt.json sealed trait JsonValue -case class JsonObject(members: Map[String, JsonValue]) extends JsonValue +final case class JsonObject(members: Map[String, JsonValue]) extends JsonValue final case class JsonString(value: String) extends JsonValue diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala index 12dc415..9b08f09 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -9,9 +9,7 @@ import org.janjaali.sprayjwt.json.JsonObject * * @param headers set of contained headers */ -sealed abstract case class JoseHeader private ( - headers: Set[Header] -) { +sealed abstract case class JoseHeader private (headers: Set[Header]) { def asJson: JsonObject = { JsonObject(headers.map { header => diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala index 1e399b8..ac31a8d 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala @@ -45,10 +45,7 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ val joseHeaders = sut( - List( - Header.Private("name", 1), - Header.Private("name", 3) - ) + List(Header.Private("name", 1), Header.Private("name", 3)) ) joseHeaders.headers should contain only Header.Private("name", 3) @@ -61,9 +58,14 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { "should result in a JSON object." in { forAll(ScalaCheckGenerators.joseHeader) { joseHeader => - joseHeader.asJson shouldBe a[JsonObject] - fail("add some more tests for this method.") + val jsonObject = joseHeader.asJson + + val expectedMembers = joseHeader.headers.map { header => + header.name -> header.valueAsJson + } + + jsonObject.members should contain theSameElementsAs expectedMembers } } } From eba71493dd2fdfe52d55f21573b7c6918ab7ae37 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 13 Apr 2021 22:10:26 +0200 Subject: [PATCH 30/43] WIP. --- .editorconfig | 9 ------- .../sprayjwt/algorithms/Algorithm.scala | 24 ++++++++++++++----- 2 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c6c8b36..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index cf40327..0daefab 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -106,15 +106,21 @@ object Algorithm { */ object Hmac { - case object Hs256 extends Hmac { + /** HMAC using SHA-256 + */ + final case object Hs256 extends Hmac { override val hashingAlgorithmName = "HMACSHA256" } - case object Hs384 extends Hmac { + /** HMAC using SHA-384 + */ + final case object Hs384 extends Hmac { override val hashingAlgorithmName = "HMACSHA384" } - case object Hs512 extends Hmac { + /** HMAC using SHA-512 + */ + final case object Hs512 extends Hmac { override val hashingAlgorithmName = "HMACSHA512" } } @@ -199,15 +205,21 @@ object Algorithm { */ object Rsa { - case object Rs256 extends Rsa { + /** RSASSA-PKCS1-v1_5 using SHA-256 + */ + final case object Rs256 extends Rsa { override protected def hashingAlgorithmName: String = "SHA256withRSA" } - case object Rs384 extends Rsa { + /** RSASSA-PKCS1-v1_5 using SHA-384 + */ + final case object Rs384 extends Rsa { override protected def hashingAlgorithmName: String = "SHA384withRSA" } - case object Rs512 extends Rsa { + /** RSASSA-PKCS1-v1_5 using SHA-512 + */ + final case object Rs512 extends Rsa { override protected def hashingAlgorithmName: String = "SHA512withRSA" } } From d68043de8f4779bde5bf2004c2e85cde2cc46d37 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 20 Apr 2021 22:17:37 +0200 Subject: [PATCH 31/43] WIP. --- build.sbt | 26 +++--- .../sprayjson/SprayJsonStringSerializer.scala | 28 ++++++ .../sprayjson/SprayJsonAlgorithmSpec.scala | 15 ++++ .../src/main/resources/application.conf | 11 --- .../sprayjwt/akkahttp/ApiRoutes.scala | 21 ----- .../janjaali/sprayjwt/akkahttp/Server.scala | 46 ---------- .../resource/SecuredResourceRoute.scala | 20 ----- .../sprayjwt/akkahttp/token/TokenRoutes.scala | 20 ----- .../scala/org/janjaali/sprayjwt/Jwt.scala | 41 +++++++-- .../sprayjwt/algorithms/Algorithm.scala | 89 ++++++++++++++++--- .../janjaali/sprayjwt/algorithms/Secret.scala | 4 + .../sprayjwt/algorithms/dan.worksheet.sc | 10 ++- .../sprayjwt/encoder/Base64Encoder.scala | 28 +++--- .../sprayjwt/json/CommonJsonWriters.scala | 44 ++++++--- .../sprayjwt/json/JsonStringSerializer.scala | 12 ++- .../janjaali/sprayjwt/json/JsonValue.scala | 7 +- .../janjaali/sprayjwt/jws/JoseHeader.scala | 8 +- .../janjaali/sprayjwt/jws/JwsPayload.scala | 2 +- .../sprayjwt/algorithms/AlgorithmSpec.scala | 47 +++++++++- 19 files changed, 281 insertions(+), 198 deletions(-) create mode 100644 spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala create mode 100644 spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala delete mode 100644 spray-jwt-akka-http-test/src/main/resources/application.conf delete mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala delete mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala delete mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala delete mode 100644 spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala diff --git a/build.sbt b/build.sbt index 93866f1..32c5a5b 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,9 @@ lazy val sprayJwt = (project in file("spray-jwt")) name := "spray-jwt", organization := "com.github.janjaali", version := "1.0.0", - licenses := Seq("MIT License" -> url("https://opensource.org/licenses/MIT")), + licenses := Seq( + "MIT License" -> url("https://opensource.org/licenses/MIT") + ), homepage := Some(url("https://github.com/janjaali/spray-jwt")), scmInfo := Some( ScmInfo( @@ -35,30 +37,22 @@ lazy val sprayJwt = (project in file("spray-jwt")) }, libraryDependencies ++= Seq( // JSON - "io.spray" %% "spray-json" % "1.3.3", - + "io.spray" %% "spray-json" % "1.3.6", // Encryption "org.bouncycastle" % "bcpkix-jdk15on" % "1.58", - // Test "org.scalatest" %% "scalatest" % "3.2.6" % Test, - // Property based tests "org.scalacheck" %% "scalacheck" % "1.15.3" % Test, "org.scalatestplus" %% "scalacheck-1-15" % "3.2.6.0" % Test ) ) -lazy val sprayJwtAkkaHttpTest = (project in file("spray-jwt-akka-http-test")) - .dependsOn(sprayJwt) - .settings( - name := "spray-jwt-akka-http-test", +lazy val sprayJsonSupport = (project in file("spray-json-support")) + .dependsOn(sprayJwt % "test->test;compile->compile") + .settings { libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-http" % "10.0.11", - "com.typesafe.akka" %% "akka-http-testkit" % "10.0.11" % Test, - "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.11", - - "com.typesafe.scala-logging" %% "scala-logging" % "3.8.0", - "ch.qos.logback" % "logback-classic" % "1.2.3" + // Supported JSON library + "io.spray" %% "spray-json" % "1.3.6" ) - ) + } diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala new file mode 100644 index 0000000..2c01e58 --- /dev/null +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala @@ -0,0 +1,28 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.json._ +import spray.json._ + +object SprayJsonStringSerializer extends JsonStringSerializer { + + override def serialize(jsonValue: JsonValue): String = { + sprayJsonValue(jsonValue).compactPrint + } + + private def sprayJsonValue(jsonValue: JsonValue): JsValue = { + jsonValue match { + case JsonObject(members) => + JsObject( + members.map { case (key, value) => + key -> sprayJsonValue(value) + }.toMap + ) + case JsonString(value) => + JsString(value) + case JsonNumber(value) => + JsNumber(value) + case JsonBoolean(value) => + JsBoolean(value) + } + } +} diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala new file mode 100644 index 0000000..07433f3 --- /dev/null +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala @@ -0,0 +1,15 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.algorithms.{Algorithm, AlgorithmSpec} + +final class SprayJsonAlgorithmSpec extends AlgorithmSpec { + + import SprayJsonStringSerializer.Implicits._ + + "SprayJsonAlgorithm" - { + + behave like signWithHmacAlgorithm( + Algorithm.Hmac.Hs256 + ) + } +} diff --git a/spray-jwt-akka-http-test/src/main/resources/application.conf b/spray-jwt-akka-http-test/src/main/resources/application.conf deleted file mode 100644 index 0300ed1..0000000 --- a/spray-jwt-akka-http-test/src/main/resources/application.conf +++ /dev/null @@ -1,11 +0,0 @@ -server { - port = 8080 -} - -api { - rest { - routes { - secret = "secret" - } - } -} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala deleted file mode 100644 index 682fa18..0000000 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/ApiRoutes.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.janjaali.sprayjwt.akkahttp - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.{Directives, ExceptionHandler, Route, RouteConcatenation} -import com.typesafe.scalalogging.LazyLogging - -class ApiRoutes(routes: Route*) extends Directives with LazyLogging { - def route: Route = { - handleExceptions(defaultExceptionHandler) { - pathPrefix("api" / "rest") { - RouteConcatenation.concat(routes:_*) - } - } - } - - val defaultExceptionHandler = ExceptionHandler { - case ex: Exception => - logger.error(s"Catch unhandled exception in defaultExceptionHandler", ex) - complete(StatusCodes.InternalServerError, "ups") - } -} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala deleted file mode 100644 index c449fa9..0000000 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/Server.scala +++ /dev/null @@ -1,46 +0,0 @@ -package org.janjaali.sprayjwt.akkahttp - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.stream.ActorMaterializer -import com.typesafe.config.ConfigFactory -import com.typesafe.scalalogging.LazyLogging -import org.janjaali.sprayjwt.akkahttp.resource.SecuredResourceRoute -import org.janjaali.sprayjwt.akkahttp.token.TokenRoutes - -import scala.concurrent.ExecutionContextExecutor -import scala.util.{Failure, Success} - -object Server extends LazyLogging { - def main(args: Array[String]): Unit = { - logger.info("Starting spray-jwt-akka-http Server") - - implicit val system: ActorSystem = ActorSystem("spray-jwt-akka-http-test") - implicit val executionContext: ExecutionContextExecutor = system.dispatcher - implicit val materializer: ActorMaterializer = ActorMaterializer.create(system) - - val config = ConfigFactory.defaultApplication() - - val host = "0.0.0.0" - val port = config.getInt("server.port") - - val secret = config.getString("api.rest.routes.secret") - - val tokenRoutes = new TokenRoutes(secret) - - val securedResourceRoute = new SecuredResourceRoute(secret) - - val routes = new ApiRoutes( - tokenRoutes.routes, - securedResourceRoute.routes - ).route - - Http().bindAndHandle(routes, host, port) - .onComplete { - case Success(_) => logger.info(s"Server starter on $host:$port") - case Failure(ex) => - logger.error(s"Server failed to start due to ${ex.getMessage}", ex) - System.exit(1) - } - } -} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala deleted file mode 100644 index 875d663..0000000 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/resource/SecuredResourceRoute.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.janjaali.sprayjwt.akkahttp.resource - -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.server.{Directives, Route} -import org.janjaali.sprayjwt.LegacyJwt - -class SecuredResourceRoute(secret: String) extends Directives { - val routes: Route = Route { - pathPrefix("secured" / "resource") { - get { - rejectEmptyResponse { - parameter("token") { token => - val payload = LegacyJwt.decode(token, secret) - complete(payload) - } - } - } - } - } -} diff --git a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala b/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala deleted file mode 100644 index 303d561..0000000 --- a/spray-jwt-akka-http-test/src/main/scala/org/janjaali/sprayjwt/akkahttp/token/TokenRoutes.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.janjaali.sprayjwt.akkahttp.token - -import akka.http.scaladsl.server.{Directives, Route} -import org.janjaali.sprayjwt.LegacyJwt -import spray.json.{JsObject, JsString} -import org.janjaali.sprayjwt.algorithms.Algorithm - -class TokenRoutes(secret: String) extends Directives { - val routes: Route = Route { - path("token") { - rejectEmptyResponse { - get { - val payload = JsObject("dance" -> JsString("in the rain")) - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs256) - complete(jwt) - } - } - } - } -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala index a674470..035087a 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala @@ -17,6 +17,7 @@ import scala.util.{Success, Try} import org.janjaali.sprayjwt.json._ import org.janjaali.sprayjwt.jwt.JwtClaimsSet import org.janjaali.sprayjwt.jws.JwsPayload +import org.bouncycastle.util.encoders /** TODO: */ @@ -79,7 +80,9 @@ private case class JoseHeader(algorithm: Algorithm) { JsonObject( Map( "typ" -> JsonString(this.typ), - "alg" -> JsonString("algorithm.name") // TODO: JSON Writer for JoseHeader needed + "alg" -> JsonString( + "algorithm.name" + ) // TODO: JSON Writer for JoseHeader needed ) ) } @@ -91,7 +94,10 @@ object Jwt { claims: JwtClaimsSet, algorithm: Algorithm, secret: String - )(implicit jsonStringSerializer: JsonStringSerializer): Jwt = { + )(implicit + serializer: JsonStringSerializer, + base64Encoder: Base64Encoder + ): Jwt = { Jwt( joseHeader = JoseHeader(algorithm), @@ -102,13 +108,26 @@ object Jwt { private def apply( joseHeader: JoseHeader, jwsPayload: JwsPayload - )(implicit jsonStringSerializer: JsonStringSerializer): Jwt = { - - val joseHeaderJson = joseHeader.asJson - val joseHeaderJsonBase64Encoded = Base64Encoder.encode(joseHeaderJson) + )(implicit + serializer: JsonStringSerializer, + base64Encoder: Base64Encoder + ): Jwt = { + + val joseHeaderJsonBase64Encoded = { + base64Encoder.encode { + serializer.serialize { + joseHeader.asJson + } + } + } - val jwsPayloadJson = jwsPayload.asJson - val jwsPayloadJsonBase64Encoded = Base64Encoder.encode(jwsPayloadJson) + val jwsPayloadJsonBase64Encoded = { + base64Encoder.encode { + serializer.serialize { + jwsPayload.asJson + } + } + } new Jwt( joseHeader = joseHeaderJsonBase64Encoded, @@ -222,6 +241,9 @@ object LegacyJwt { val algorithm = getAlgorithmFromHeader(header) + implicit val serializeJson: JsonValue => String = ??? // new added + implicit val base64encoder: Base64Encoder = ??? // new added + if (!algorithm.validate(signature, data, secret)) { throw new InvalidSignatureException() } @@ -253,6 +275,9 @@ object LegacyJwt { val encodedData = s"$encodedHeader.$encodedPayload" + implicit val serializeJson: JsonValue => String = ??? // new added + implicit val base64encoder: Base64Encoder = ??? // new added + val signature = algorithm.sign(encodedData, secret) s"$encodedData.$signature" } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 0daefab..575bc65 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -6,6 +6,7 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} +import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} import org.janjaali.sprayjwt.jws.{JoseHeader, JwsPayload, JwsSignature} import java.io.{IOException, StringReader} @@ -26,6 +27,9 @@ sealed trait Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder ): JwsSignature /** Signs data. // TODO: legacy? @@ -34,8 +38,15 @@ sealed trait Algorithm { * @param secret the secret to use for signing the data * @return signed data */ - def sign(data: String, secret: String): String - + def sign( + data: String, + secret: String + )(implicit + serializer: JsonValue => String, + base64encoder: Base64Encoder + ): String + + // TODO: Check if serializer is needed // TODO: Idea for new method signature // def sign(jwsProtectedHeader: JwsProtectedHeader, jwsPayload: JwsPayload): JwsSignature @@ -46,7 +57,14 @@ sealed trait Algorithm { * @param secret the secret to use for validation * @return true if signature is valid, otherwise returns false */ - def validate(signature: String, data: String, secret: String): Boolean + def validate( + signature: String, + data: String, + secret: String + )(implicit + serializer: JsonValue => String, + base64encoder: Base64Encoder + ): Boolean } /** Provides algorithms. @@ -58,7 +76,7 @@ object Algorithm { */ sealed trait Hmac extends Algorithm { - private val provider = "SunJCE" + private val provider = "SunJCE" // TODO: Check if needed to create MAC's protected def hashingAlgorithmName: String @@ -66,20 +84,53 @@ object Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder ): JwsSignature = { + val mac = Mac.getInstance(hashingAlgorithmName, provider) val key = new SecretKeySpec(secret.asByteArray, hashingAlgorithmName) - val mac = Mac.getInstance(hashingAlgorithmName, provider) mac.init(key) - val signature = mac.doFinal(???) + println(s"JOSE Header JSON: ${serializeJson(joseHeader.asJson)}") + + val base64UrlEncodedJoseHeader = { + base64encoder.encode { + serializeJson { + joseHeader.asJson + } + } + } + + println(s"Base64EncodedJoseHeader: '$base64UrlEncodedJoseHeader'.") + + val base64UrlEncodedJwsPayload = { + base64encoder.encode { + serializeJson { + jwsPayload.asJson + } + } + } + + val inputToBeSigned = { + s"$base64UrlEncodedJoseHeader.$base64UrlEncodedJwsPayload" + } + + val signature = mac.doFinal(inputToBeSigned.getBytes("UTF-8")) - JwsSignature(Base64Encoder.encode(signature)) + JwsSignature(base64encoder.encode(signature)) } // TODO: Check implementation - override def sign(data: String, secret: String): String = { + override def sign( + data: String, + secret: String + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder + ): String = { val secretAsByteArray = ByteEncoder.getBytes(secret) val secretKey = new SecretKeySpec(secretAsByteArray, hashingAlgorithmName) @@ -89,14 +140,18 @@ object Algorithm { val mac = Mac.getInstance(hashingAlgorithmName, provider) mac.init(secretKey) val signAsByteArray = mac.doFinal(dataAsByteArray) - Base64Encoder.encode(signAsByteArray) + base64encoder.encode(signAsByteArray) } // TODO: Check implementation + // TODO: Check if serializer is needed override def validate( signature: String, data: String, secret: String + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder ): Boolean = { sign(data, secret) == signature } @@ -138,13 +193,22 @@ object Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder ): JwsSignature = { ??? } // TODO: Check implementation - override def sign(data: String, secret: String): String = { + override def sign( + data: String, + secret: String + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder + ): String = { val key = getPrivateKey(secret) @@ -154,7 +218,7 @@ object Algorithm { signature.initSign(key) signature.update(dataByteArray) val signatureByteArray = signature.sign - Base64Encoder.encode(signatureByteArray) + base64encoder.encode(signatureByteArray) } // TODO: Check implementation @@ -162,6 +226,9 @@ object Algorithm { signature: String, data: String, secret: String + )(implicit + serializeJson: JsonValue => String, + base64encoder: Base64Encoder ): Boolean = { val key = getPublicKey(secret) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala index 8d795a5..5ebc9d1 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Secret.scala @@ -4,6 +4,10 @@ package org.janjaali.sprayjwt.algorithms */ final case class Secret(value: String) extends AnyVal { + /** Encodes this secret value as a byte array using the UTF-8 charset. + * + * @return byte-array + */ def asByteArray: Array[Byte] = { value.getBytes("UTF-8") } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc index 8cc0227..2e47fe9 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc @@ -1,11 +1,15 @@ val encoder = java.util.Base64.getEncoder() -val urlEncoder = java.util.Base64.getUrlEncoder() +val urlEncoder = java.util.Base64.getEncoder() -val json = """{"typ":"JWT","alg":"HS256"}""" +val json = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" val jsonBytes = json.getBytes("UTF-8") val encodedBytes = urlEncoder.encode(jsonBytes) -new String(encodedBytes) +val result = new String(encodedBytes) + +val expected = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" + +result == expected diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala index b8479af..c544af2 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala @@ -3,11 +3,13 @@ package org.janjaali.sprayjwt.encoder import java.util.Base64 import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -/** Base64Encoder utility class. - */ -private[sprayjwt] object Base64Encoder { +// TODO: Remove JSON stuff out of here. + +// TODO: Probably rename to Base64UrlEncoder. - private lazy val base64Encoder: Base64.Encoder = Base64.getEncoder +sealed trait Base64Encoder { + + private lazy val base64Encoder: Base64.Encoder = Base64.getUrlEncoder /** Encodes text to a Base64 encoded String. * @@ -19,20 +21,6 @@ private[sprayjwt] object Base64Encoder { encode(textAsByteArray) } - /** Encodes JSON value to a Base64 encoded String. - * - * @param jsonValue json value that should be encoded - * @param jsonStringSerializer Serializer that is used to serialize the json - * value to a String before Base64 encoding sets - * in - * @return Base64 encoded JSON value - */ - def encode( - jsonValue: JsonValue - )(implicit jsonStringSerializer: JsonStringSerializer): String = { - jsonStringSerializer.serialize(jsonValue) - } - /** Encodes a ByteArray as String. * * @param byteArray the ByteArray to encode as String @@ -42,3 +30,7 @@ private[sprayjwt] object Base64Encoder { base64Encoder.encodeToString(byteArray).replaceAll("=", "") } } + +/** Base64Encoder utility class. + */ +private[sprayjwt] object Base64Encoder extends Base64Encoder // TODO: Check if needed diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala index 14a1d65..c51911c 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala @@ -24,6 +24,14 @@ object CommonJsonWriters { CommonJsonWriters.longJsonWriter } + /** Constructs JSON writer for Boolean types. + * + * @return JSON writer + */ + implicit def booleanJsonWriter: JsonWriter[Boolean] = { + CommonJsonWriters.booleanJsonWriter + } + /** Constructs JSON writer for String types. * * @return JSON writer @@ -38,7 +46,11 @@ object CommonJsonWriters { * @return JSON writer */ def intJsonWriter: JsonWriter[Int] = { - jsonNumberWriter[Int] + new JsonWriter[Int] { + override def write(value: Int): JsonValue = { + JsonNumber(BigDecimal(value)) + } + } } /** Constructs JSON writer for Long types. @@ -46,7 +58,23 @@ object CommonJsonWriters { * @return JSON writer */ def longJsonWriter: JsonWriter[Long] = { - jsonNumberWriter[Long] + new JsonWriter[Long] { + override def write(value: Long): JsonValue = { + JsonNumber(value) + } + } + } + + /** Constructs JSON writer for Boolean types. + * + * @return JSON writer + */ + def booleanJsonWriter: JsonWriter[Boolean] = { + new JsonWriter[Boolean] { + override def write(value: Boolean): JsonValue = { + JsonBoolean(value) + } + } } /** Constructs JSON writer for String types. @@ -75,16 +103,4 @@ object CommonJsonWriters { } } } - - /** Constructs JSON writer (i.e. JSON number) for numeric types. - * - * @return JSON writer - */ - private def jsonNumberWriter[T: Numeric]: JsonWriter[T] = { - new JsonWriter[T] { - override def write(value: T): JsonValue = { - JsonNumber(value) - } - } - } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala index 027779f..c946636 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -1,9 +1,15 @@ package org.janjaali.sprayjwt.json - -// TODO: Not tested yet! +// TODO: Docs. trait JsonStringSerializer { - def serialize(jsonObject: JsonValue): String + final object Implicits { + + implicit def implicitSerialize(json: JsonValue): String = { + serialize(json) + } + } + + def serialize(json: JsonValue): String } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 9e99cab..2a5d5b3 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -1,9 +1,14 @@ package org.janjaali.sprayjwt.json +// TODO: Check accessibility. +// TODO: Add docs. + sealed trait JsonValue final case class JsonObject(members: Map[String, JsonValue]) extends JsonValue final case class JsonString(value: String) extends JsonValue -final case class JsonNumber[T: Numeric](value: T) extends JsonValue +final case class JsonNumber(value: BigDecimal) extends JsonValue + +final case class JsonBoolean(value: Boolean) extends JsonValue diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala index 9b08f09..931c883 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -12,9 +12,11 @@ import org.janjaali.sprayjwt.json.JsonObject sealed abstract case class JoseHeader private (headers: Set[Header]) { def asJson: JsonObject = { - JsonObject(headers.map { header => - header.name -> header.valueAsJson - }.toMap) + JsonObject( + headers.map { header => + header.name -> header.valueAsJson + }.toMap + ) } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala index 99caed9..90d5960 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JwsPayload.scala @@ -9,7 +9,7 @@ import org.janjaali.sprayjwt.jwt.JwtClaimsSet */ final case class JwsPayload(claimsSet: JwtClaimsSet) { - /** JSON representation of this JWS payload. + /** JSON representation of this JWS Payload. * * @return JSON object */ diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala index 956be3a..c79028f 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala @@ -1,8 +1,51 @@ package org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.encoder.Base64Encoder +import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ +import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} +import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload} +import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.janjaali.sprayjwt.jws.JwsSignature -class AlgorithmSpec extends ScalaTestSpec { +trait AlgorithmSpec extends ScalaTestSpec { - // TODO: Add some tests + implicit val base64UrlEncoder: Base64Encoder = Base64Encoder + + protected def signWithHmacAlgorithm( + algorithm: Algorithm + )(implicit serializeJson: JsonValue => String) = { + + s"Algorithm '${algorithm}'" - { + + "should sign JOSE Header and JWS Payload with a specified secret." in { + + val joseHeader = JoseHeader( + Seq( + Header.Type(Header.Type.Value.Jwt), + // TODO: That's weird, how can we set the algorithm and then just + // sign with another one? + Header.Algorithm(algorithm) + ) + ) + + val jwsPayload = JwsPayload( + JwtClaimsSet( + Seq( + // TODO: Maybe add an iss type? + Claim.Private(name = "iss", value = "joe"), + Claim.ExpirationTime(NumericDate(1300819380L)), + Claim.Private(name = "http://example.com/is_root", value = true) + ) + ) + ) + + val secret = Secret("secret value") + + algorithm.sign(joseHeader, jwsPayload, secret) shouldBe JwsSignature( + "dance" + ) + } + } + } } From 82ae155a887d9e094f69ae9271b027081988e79b Mon Sep 17 00:00:00 2001 From: Janjaali Date: Tue, 20 Apr 2021 23:32:15 +0200 Subject: [PATCH 32/43] WIP --- .../sprayjson/SprayJsonAlgorithmSpec.scala | 4 +- .../scala/org/janjaali/sprayjwt/Jwt.scala | 14 ++-- .../sprayjwt/algorithms/Algorithm.scala | 28 ++++---- .../sprayjwt/algorithms/dan.worksheet.sc | 15 ---- .../sprayjwt/encoder/Base64Encoder.scala | 36 ---------- .../sprayjwt/encoder/Base64UrlEncoder.scala | 33 +++++++++ .../sprayjwt/algorithms/AlgorithmSpec.scala | 69 +++++++++++-------- .../sprayjwt/encoder/Base64EncoderSpec.scala | 20 ------ .../encoder/Base64UrlEncoderSpec.scala | 37 ++++++++++ 9 files changed, 131 insertions(+), 125 deletions(-) delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoderSpec.scala diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala index 07433f3..08aa9ea 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala @@ -8,8 +8,6 @@ final class SprayJsonAlgorithmSpec extends AlgorithmSpec { "SprayJsonAlgorithm" - { - behave like signWithHmacAlgorithm( - Algorithm.Hmac.Hs256 - ) + verifyWithHmac256Algorithm } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala index 035087a..46b98e5 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala @@ -4,7 +4,7 @@ import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider import org.janjaali.sprayjwt.algorithms.Algorithm -import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder} +import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64UrlEncoder} import org.janjaali.sprayjwt.exceptions.{ InvalidJwtException, InvalidSignatureException @@ -96,7 +96,7 @@ object Jwt { secret: String )(implicit serializer: JsonStringSerializer, - base64Encoder: Base64Encoder + base64Encoder: Base64UrlEncoder ): Jwt = { Jwt( @@ -110,7 +110,7 @@ object Jwt { jwsPayload: JwsPayload )(implicit serializer: JsonStringSerializer, - base64Encoder: Base64Encoder + base64Encoder: Base64UrlEncoder ): Jwt = { val joseHeaderJsonBase64Encoded = { @@ -242,7 +242,7 @@ object LegacyJwt { val algorithm = getAlgorithmFromHeader(header) implicit val serializeJson: JsonValue => String = ??? // new added - implicit val base64encoder: Base64Encoder = ??? // new added + implicit val base64encoder: Base64UrlEncoder = ??? // new added if (!algorithm.validate(signature, data, secret)) { throw new InvalidSignatureException() @@ -271,12 +271,12 @@ object LegacyJwt { val encodedHeader = getEncodedHeader(algorithm) val encodedPayload = - Base64Encoder.encode(payloadWithReservedClaims.toString) + Base64UrlEncoder.encode(payloadWithReservedClaims.toString) val encodedData = s"$encodedHeader.$encodedPayload" implicit val serializeJson: JsonValue => String = ??? // new added - implicit val base64encoder: Base64Encoder = ??? // new added + implicit val base64encoder: Base64UrlEncoder = ??? // new added val signature = algorithm.sign(encodedData, secret) s"$encodedData.$signature" @@ -290,7 +290,7 @@ object LegacyJwt { private def getEncodedHeader(algorithm: Algorithm): String = { val header = JwtHeader(algorithm).toJson.toString - Base64Encoder.encode(header) + Base64UrlEncoder.encode(header) } private def getReversedClaims(jwtClaims: JwtClaims): Map[String, JsValue] = { diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 575bc65..6b68557 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -5,7 +5,11 @@ import javax.crypto.spec.SecretKeySpec import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} -import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64Encoder, ByteEncoder} +import org.janjaali.sprayjwt.encoder.{ + Base64Decoder, + Base64UrlEncoder, + ByteEncoder +} import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} import org.janjaali.sprayjwt.jws.{JoseHeader, JwsPayload, JwsSignature} @@ -29,7 +33,7 @@ sealed trait Algorithm { secret: Secret )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): JwsSignature /** Signs data. // TODO: legacy? @@ -43,7 +47,7 @@ sealed trait Algorithm { secret: String )(implicit serializer: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): String // TODO: Check if serializer is needed @@ -63,7 +67,7 @@ sealed trait Algorithm { secret: String )(implicit serializer: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): Boolean } @@ -86,7 +90,7 @@ object Algorithm { secret: Secret )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): JwsSignature = { val mac = Mac.getInstance(hashingAlgorithmName, provider) @@ -94,8 +98,6 @@ object Algorithm { mac.init(key) - println(s"JOSE Header JSON: ${serializeJson(joseHeader.asJson)}") - val base64UrlEncodedJoseHeader = { base64encoder.encode { serializeJson { @@ -104,8 +106,6 @@ object Algorithm { } } - println(s"Base64EncodedJoseHeader: '$base64UrlEncodedJoseHeader'.") - val base64UrlEncodedJwsPayload = { base64encoder.encode { serializeJson { @@ -129,7 +129,7 @@ object Algorithm { secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): String = { val secretAsByteArray = ByteEncoder.getBytes(secret) @@ -151,7 +151,7 @@ object Algorithm { secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): Boolean = { sign(data, secret) == signature } @@ -195,7 +195,7 @@ object Algorithm { secret: Secret )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): JwsSignature = { ??? @@ -207,7 +207,7 @@ object Algorithm { secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): String = { val key = getPrivateKey(secret) @@ -228,7 +228,7 @@ object Algorithm { secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64Encoder + base64encoder: Base64UrlEncoder ): Boolean = { val key = getPublicKey(secret) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc deleted file mode 100644 index 2e47fe9..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/dan.worksheet.sc +++ /dev/null @@ -1,15 +0,0 @@ -val encoder = java.util.Base64.getEncoder() - -val urlEncoder = java.util.Base64.getEncoder() - -val json = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" - -val jsonBytes = json.getBytes("UTF-8") - -val encodedBytes = urlEncoder.encode(jsonBytes) - -val result = new String(encodedBytes) - -val expected = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" - -result == expected diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala deleted file mode 100644 index c544af2..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Encoder.scala +++ /dev/null @@ -1,36 +0,0 @@ -package org.janjaali.sprayjwt.encoder - -import java.util.Base64 -import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} - -// TODO: Remove JSON stuff out of here. - -// TODO: Probably rename to Base64UrlEncoder. - -sealed trait Base64Encoder { - - private lazy val base64Encoder: Base64.Encoder = Base64.getUrlEncoder - - /** Encodes text to a Base64 encoded String. - * - * @param text the text to encode - * @return Base64 encoded String - */ - def encode(text: String): String = { - val textAsByteArray = text.getBytes("UTF-8") - encode(textAsByteArray) - } - - /** Encodes a ByteArray as String. - * - * @param byteArray the ByteArray to encode as String - * @return String encoded ByteArray - */ - def encode(byteArray: Array[Byte]): String = { - base64Encoder.encodeToString(byteArray).replaceAll("=", "") - } -} - -/** Base64Encoder utility class. - */ -private[sprayjwt] object Base64Encoder extends Base64Encoder // TODO: Check if needed diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala new file mode 100644 index 0000000..edf5cf9 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala @@ -0,0 +1,33 @@ +package org.janjaali.sprayjwt.encoder + +import java.util.Base64 + +/** Default Base64 URL encoder that uses internally the [[java.util.Base64]] URL + * encoder. + */ +private[sprayjwt] trait Base64UrlEncoder { + + private lazy val encoder: Base64.Encoder = Base64.getUrlEncoder.withoutPadding + + /** Encodes text to a Base64 URL encoded String. + * + * @param text text that should be encoded + * @return Base64 URL encoded String + */ + def encode(text: String): String = { + encode(text.getBytes("UTF-8")) + } + + /** Encodes a byte-array as Base64 URL encoded String. + * + * @param byteArray ByteArray to encode as String + * @return String encoded ByteArray + */ + def encode(byteArray: Array[Byte]): String = { + encoder.encodeToString(byteArray) + } +} + +/** Base64 URL encoder utilities. + */ +private[sprayjwt] object Base64UrlEncoder extends Base64UrlEncoder diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala index c79028f..26b73f6 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala @@ -1,51 +1,60 @@ package org.janjaali.sprayjwt.algorithms -import org.janjaali.sprayjwt.encoder.Base64Encoder +import org.janjaali.sprayjwt.encoder.Base64UrlEncoder import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload} +import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} import org.janjaali.sprayjwt.tests.ScalaTestSpec -import org.janjaali.sprayjwt.jws.JwsSignature trait AlgorithmSpec extends ScalaTestSpec { - implicit val base64UrlEncoder: Base64Encoder = Base64Encoder + implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder - protected def signWithHmacAlgorithm( - algorithm: Algorithm - )(implicit serializeJson: JsonValue => String) = { + protected def verifyWithHmac256Algorithm(implicit + serializeJson: JsonValue => String + ): Unit = { - s"Algorithm '${algorithm}'" - { - - "should sign JOSE Header and JWS Payload with a specified secret." in { + verifyWithAlgorithm( + algorithm = Algorithm.Hmac.Hs256, + expectedSignature = JwsSignature( + "jUzTJEnlFeTXDUPp9vJMwoalvXJ55IZ6DaBExN08UtA" + ) + ) + } - val joseHeader = JoseHeader( - Seq( - Header.Type(Header.Type.Value.Jwt), - // TODO: That's weird, how can we set the algorithm and then just - // sign with another one? - Header.Algorithm(algorithm) - ) + private def verifyWithAlgorithm( + algorithm: Algorithm, + expectedSignature: JwsSignature + )(implicit + serializeJson: JsonValue => String + ): Unit = { + + "Verify serializer with HS256 algorithm." in { + + val joseHeader = JoseHeader( + Seq( + Header.Type(Header.Type.Value.Jwt), + // TODO: That's weird, how can we set the algorithm and then just + // sign with another one? + Header.Algorithm(algorithm) ) + ) - val jwsPayload = JwsPayload( - JwtClaimsSet( - Seq( - // TODO: Maybe add an iss type? - Claim.Private(name = "iss", value = "joe"), - Claim.ExpirationTime(NumericDate(1300819380L)), - Claim.Private(name = "http://example.com/is_root", value = true) - ) + val jwsPayload = JwsPayload( + JwtClaimsSet( + Seq( + // TODO: Maybe add an iss type? + Claim.Private(name = "iss", value = "joe"), + Claim.ExpirationTime(NumericDate(1300819380L)), + Claim.Private(name = "http://example.com/is_root", value = true) ) ) + ) - val secret = Secret("secret value") + val secret = Secret("secret value") - algorithm.sign(joseHeader, jwsPayload, secret) shouldBe JwsSignature( - "dance" - ) - } + algorithm.sign(joseHeader, jwsPayload, secret) shouldBe expectedSignature } } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala deleted file mode 100644 index cb9405f..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64EncoderSpec.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.janjaali.sprayjwt.encoder - -import org.scalatest.funspec.AnyFunSpec - -class Base64EncoderSpec extends AnyFunSpec { - - describe("Base64Encoder") { - it("encodes text as Base64 encoded text") { - val encodedString = Base64Encoder.encode("""{"alg":"HS256","typ":"JWT"}""") - assert(encodedString == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") - } - - it("encodes byteArray as Base64 encoded text") { - val byteArray = """{"alg":"HS256","typ":"JWT"}""".getBytes("UTF-8") - val encodedString = Base64Encoder.encode(byteArray) - assert(encodedString == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") - } - } - -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoderSpec.scala new file mode 100644 index 0000000..fb1df8d --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoderSpec.scala @@ -0,0 +1,37 @@ +package org.janjaali.sprayjwt.encoder + +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +final class Base64UrlUrlEncoderSpec extends ScalaTestSpec { + + "Base64UrlEncoder" - { + + val sut = Base64UrlEncoder + + "encodes text as Base64 URL encoded String." in { + + val text = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" + + sut.encode(text) shouldBe "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" + } + + "encodes text as Base64 URL encoded String without padding." in { + + val text = { + "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}" + } + + sut.encode(text) shouldBe { + "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly" + + "9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" + } + } + + "encoded byte array as Base64 URL encoded String." in { + + val text = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}".getBytes("UTF-8") + + sut.encode(text) shouldBe "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" + } + } +} From 8e2a69e4d22240638c54e84997e57fae158f79a6 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Wed, 21 Apr 2021 23:37:29 +0200 Subject: [PATCH 33/43] WIP. --- .../sprayjson/SprayJsonAlgorithmSpec.scala | 13 - .../SprayJsonStringSerializerSpec.scala | 21 ++ .../scala/org/janjaali/sprayjwt/Jwt.scala | 328 ----------------- .../sprayjwt/algorithms/Algorithm.scala | 155 ++++---- .../sprayjwt/encoder/Base64Decoder.scala | 32 -- .../sprayjwt/encoder/Base64UrlDecoder.scala | 33 ++ .../sprayjwt/encoder/Base64UrlEncoder.scala | 2 +- .../sprayjwt/encoder/ByteEncoder.scala | 2 + .../janjaali/sprayjwt/jws/JoseHeader.scala | 2 + .../scala/org/janjaali/sprayjwt/JwtSpec.scala | 334 ------------------ .../sprayjwt/algorithms/AlgorithmSpec.scala | 74 ++-- .../algorithms/JsonStringSerializerSpec.scala | 82 +++++ .../sprayjwt/encoder/Base64DecoderSpec.scala | 2 +- 13 files changed, 246 insertions(+), 834 deletions(-) delete mode 100644 spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala create mode 100644 spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlDecoder.scala delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala deleted file mode 100644 index 08aa9ea..0000000 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonAlgorithmSpec.scala +++ /dev/null @@ -1,13 +0,0 @@ -package org.janjaali.sprayjwt.sprayjson - -import org.janjaali.sprayjwt.algorithms.{Algorithm, AlgorithmSpec} - -final class SprayJsonAlgorithmSpec extends AlgorithmSpec { - - import SprayJsonStringSerializer.Implicits._ - - "SprayJsonAlgorithm" - { - - verifyWithHmac256Algorithm - } -} diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala new file mode 100644 index 0000000..b705ccf --- /dev/null +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala @@ -0,0 +1,21 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.algorithms.{Algorithm, JsonStringSerializerSpec} +import org.janjaali.sprayjwt.json.JsonValue +import org.janjaali.sprayjwt.json.JsonStringSerializer + +final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec { + + override final protected def jsonStringSerializer: JsonStringSerializer = { + SprayJsonStringSerializer + } + + "SprayJsonStringSerializer" - { + + verifySignWithHmac256Algorithm() + + verifySignWithHmac384Algorithm() + + verifySignWithHmac512Algorithm() + } +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala deleted file mode 100644 index 46b98e5..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/Jwt.scala +++ /dev/null @@ -1,328 +0,0 @@ -package org.janjaali.sprayjwt - -import java.security.Security - -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.janjaali.sprayjwt.algorithms.Algorithm -import org.janjaali.sprayjwt.encoder.{Base64Decoder, Base64UrlEncoder} -import org.janjaali.sprayjwt.exceptions.{ - InvalidJwtException, - InvalidSignatureException -} -import org.janjaali.sprayjwt.headers.{JwtHeader, JwtHeaderJsonWriter} -import spray.json._ - -import scala.util.{Success, Try} - -import org.janjaali.sprayjwt.json._ -import org.janjaali.sprayjwt.jwt.JwtClaimsSet -import org.janjaali.sprayjwt.jws.JwsPayload -import org.bouncycastle.util.encoders - -/** TODO: - */ -trait Claim { - - type T - - def key: String - - def value: T - - def asJson: JsonValue -} - -sealed trait Secret - -/** Represents a JWT consisting of the encoded parts: - * - *
    - *
  1. JOSE Header
  2. - *
  3. JWS Payload
  4. - *
  5. JWS Signature
  6. - *
- * - * @param jose - * @param claims - * @param signature - */ -sealed abstract case class Jwt private ( - joseHeader: String, - jwsPayload: String, - jwsSignature: String -) { - - /** Raw encoded string representation of this JWT. - * - * @return raw JWT string - */ - def raw: String = { - s"$joseHeader.$jwsPayload.$jwsSignature" - } -} - -/** Represents a JOSE Header containing the parameters describing the - * cryptographic operations and parameters employed to encode a JWT value. - * - * // TODO: Is this the JWS Protected Header? (JWS Header) - * - * @param algorithm TODO: define - */ -private case class JoseHeader(algorithm: Algorithm) { - - private val typ = "JWT" - - /** JSON representation of this JOSE Header. - * - * @return JSON object - */ - def asJson: JsonObject = { - JsonObject( - Map( - "typ" -> JsonString(this.typ), - "alg" -> JsonString( - "algorithm.name" - ) // TODO: JSON Writer for JoseHeader needed - ) - ) - } -} - -object Jwt { - - def apply( - claims: JwtClaimsSet, - algorithm: Algorithm, - secret: String - )(implicit - serializer: JsonStringSerializer, - base64Encoder: Base64UrlEncoder - ): Jwt = { - - Jwt( - joseHeader = JoseHeader(algorithm), - jwsPayload = JwsPayload(claims) - ) - } - - private def apply( - joseHeader: JoseHeader, - jwsPayload: JwsPayload - )(implicit - serializer: JsonStringSerializer, - base64Encoder: Base64UrlEncoder - ): Jwt = { - - val joseHeaderJsonBase64Encoded = { - base64Encoder.encode { - serializer.serialize { - joseHeader.asJson - } - } - } - - val jwsPayloadJsonBase64Encoded = { - base64Encoder.encode { - serializer.serialize { - jwsPayload.asJson - } - } - } - - new Jwt( - joseHeader = joseHeaderJsonBase64Encoded, - jwsPayload = jwsPayloadJsonBase64Encoded, - jwsSignature = ??? - ) {} - } -} - -/** JWT Encoder/Decoder. - */ -object LegacyJwt { - - addBouncyCastleProvider() - - /** Encodes payload as JWT. - * - * @param payload the payload to encode as JWT's payload - * @param secret the secret which is used to sign JWT - * @param algorithm the hashing algorithm used for encoding - * @return encoded JWT - */ - def encode( - payload: String, - secret: String, - algorithm: Algorithm - ): Try[String] = { - Try(encode(payload.parseJson, secret, algorithm, None)) - } - - /** Encodes payload as JWT. - * - * @param payload the payload to encode as JWT's payload - * @param secret the secret which is used to sign JWT - * @param algorithm the hashing algorithm used for encoding - * @param jwtClaims reserved JWT claims to add to payload (specified claims will overwrite equal named claims in - * payload) - * @return encoded JWT - */ - def encode( - payload: String, - secret: String, - algorithm: Algorithm, - jwtClaims: JwtClaims - ): Try[String] = { - Try(encode(payload.parseJson, secret, algorithm, Some(jwtClaims))) - } - - /** Encodes payload as JWT. - * - * @param payload the payload to encode as JWT's payload - * @param secret the secret which is used to sign JWT - * @param algorithm the hashing algorithm used for encoding - * @return encoded JWT - */ - def encode( - payload: JsValue, - secret: String, - algorithm: Algorithm - ): Try[String] = { - Try(encode(payload, secret, algorithm, None)) - } - - /** Encodes payload as JWT. - * - * @param payload the payload to encode as JWT's payload - * @param secret the secret which is used to sign JWT - * @param algorithm the hashing algorithm used for encoding - * @param jwtClaims reserved JWT claims to add to payload (specified claims will overwrite equal named claims in - * payload) - * @return encoded JWT - */ - def encode( - payload: JsValue, - secret: String, - algorithm: Algorithm, - jwtClaims: JwtClaims - ): Try[String] = { - Try(encode(payload, secret, algorithm, Some(jwtClaims))) - } - - /** Decodes JWT token as JsValue. - * - * @param token the JWT token to decode - * @param secret the secret to use to validate signature of JWT - * @return JsValue decoded JWT - */ - def decode(token: String, secret: String): Try[JsValue] = { - decodeAsString(token, secret).map(_.parseJson) - } - - /** Decodes JWT token as JsValue. - * - * @param token the JWT token to decode - * @param secret the secret to use to validate signature of JWT - * @return JsValue decoded JWT - */ - def decodeAsString(token: String, secret: String): Try[String] = { - val splitToken = token.split("\\.") - if (splitToken.length != 3) { - throw new InvalidJwtException( - "JWT must have form header.payload.signature" - ) - } - - val header = splitToken(0) - val payload = splitToken(1) - val data = s"$header.$payload" - - val signature = splitToken(2) - - val algorithm = getAlgorithmFromHeader(header) - - implicit val serializeJson: JsonValue => String = ??? // new added - implicit val base64encoder: Base64UrlEncoder = ??? // new added - - if (!algorithm.validate(signature, data, secret)) { - throw new InvalidSignatureException() - } - - val payloadDecoded = Base64Decoder.decodeAsString(payload) - Success(payloadDecoded) - } - - private def getAlgorithmFromHeader(header: String): Algorithm = { - val headerDecoded = Base64Decoder.decodeAsString(header) - val jwtHeader = headerDecoded.parseJson.convertTo[JwtHeader] - jwtHeader.algorithm - } - - private def encode( - payload: JsValue, - secret: String, - algorithm: Algorithm, - jwtClaims: Option[JwtClaims] - ): String = { - val fields = payload.asJsObject.fields - val reversedClaims = jwtClaims.map(getReversedClaims).getOrElse(Map.empty) - - val payloadWithReservedClaims = JsObject(fields ++ reversedClaims) - - val encodedHeader = getEncodedHeader(algorithm) - val encodedPayload = - Base64UrlEncoder.encode(payloadWithReservedClaims.toString) - - val encodedData = s"$encodedHeader.$encodedPayload" - - implicit val serializeJson: JsonValue => String = ??? // new added - implicit val base64encoder: Base64UrlEncoder = ??? // new added - - val signature = algorithm.sign(encodedData, secret) - s"$encodedData.$signature" - } - - private def addBouncyCastleProvider(): Unit = { - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider) - } - } - - private def getEncodedHeader(algorithm: Algorithm): String = { - val header = JwtHeader(algorithm).toJson.toString - Base64UrlEncoder.encode(header) - } - - private def getReversedClaims(jwtClaims: JwtClaims): Map[String, JsValue] = { - Seq( - "iss" -> jwtClaims.iss, - "sub" -> jwtClaims.sub, - "aud" -> jwtClaims.aud, - "exp" -> jwtClaims.exp, - "nbf" -> jwtClaims.nbf, - "isa" -> jwtClaims.isa, - "iat" -> jwtClaims.iat, - "jti" -> jwtClaims.jti - ).filter(_._2.nonEmpty) - .map(entry => entry._1 -> entry._2.get) - .map { - case (name, value: String) => name -> JsString(value) - case (name, value: Long) => name -> JsNumber(value) - case (name, values: Set[_]) => - if (values.size == 1) { - name -> JsString(values.head.asInstanceOf[String]) - } else { - name -> JsArray( - values.map(v => JsString(v.asInstanceOf[String])).toVector - ) - } - - case (name, _) => - throw new SerializationException( - s"Cannot serialize reserved claim: $name" - ) - } - .toMap - } - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 6b68557..cd419a8 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -6,7 +6,7 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} import org.janjaali.sprayjwt.encoder.{ - Base64Decoder, + Base64UrlDecoder, Base64UrlEncoder, ByteEncoder } @@ -36,39 +36,11 @@ sealed trait Algorithm { base64encoder: Base64UrlEncoder ): JwsSignature - /** Signs data. // TODO: legacy? - * - * @param data the data to sign - * @param secret the secret to use for signing the data - * @return signed data - */ - def sign( - data: String, - secret: String - )(implicit - serializer: JsonValue => String, - base64encoder: Base64UrlEncoder - ): String - - // TODO: Check if serializer is needed - // TODO: Idea for new method signature - // def sign(jwsProtectedHeader: JwsProtectedHeader, jwsPayload: JwsPayload): JwsSignature - - /** Validates signature. // TODO: legacy? - * - * @param signature the signature to validate - * @param data the data to validate signature for - * @param secret the secret to use for validation - * @return true if signature is valid, otherwise returns false - */ + // TODO: Add docs. def validate( - signature: String, data: String, - secret: String - )(implicit - serializer: JsonValue => String, - base64encoder: Base64UrlEncoder - ): Boolean + secret: Secret + )(implicit base64UrlEncoder: Base64UrlEncoder): Boolean } /** Provides algorithms. @@ -80,8 +52,6 @@ object Algorithm { */ sealed trait Hmac extends Algorithm { - private val provider = "SunJCE" // TODO: Check if needed to create MAC's - protected def hashingAlgorithmName: String override def sign( @@ -90,16 +60,11 @@ object Algorithm { secret: Secret )(implicit serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder + base64UrlEncoder: Base64UrlEncoder ): JwsSignature = { - val mac = Mac.getInstance(hashingAlgorithmName, provider) - val key = new SecretKeySpec(secret.asByteArray, hashingAlgorithmName) - - mac.init(key) - val base64UrlEncodedJoseHeader = { - base64encoder.encode { + base64UrlEncoder.encode { serializeJson { joseHeader.asJson } @@ -107,7 +72,7 @@ object Algorithm { } val base64UrlEncodedJwsPayload = { - base64encoder.encode { + base64UrlEncoder.encode { serializeJson { jwsPayload.asJson } @@ -118,42 +83,44 @@ object Algorithm { s"$base64UrlEncodedJoseHeader.$base64UrlEncodedJwsPayload" } - val signature = mac.doFinal(inputToBeSigned.getBytes("UTF-8")) - - JwsSignature(base64encoder.encode(signature)) + sign(inputToBeSigned, secret) } - // TODO: Check implementation - override def sign( + override def validate( data: String, - secret: String - )(implicit - serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder - ): String = { - - val secretAsByteArray = ByteEncoder.getBytes(secret) - val secretKey = new SecretKeySpec(secretAsByteArray, hashingAlgorithmName) + secret: Secret + )(implicit base64UrlEncoder: Base64UrlEncoder): Boolean = { + + data.split("\\.") match { + case Array( + base64UrlEncodedJoseHeader, + base64UrlEncodedJwsPayload, + base64EncodedSignature + ) => + val signedInput = { + s"$base64UrlEncodedJoseHeader.$base64UrlEncodedJwsPayload" + } - val dataAsByteArray = ByteEncoder.getBytes(data) + sign(signedInput, secret).value == base64EncodedSignature - val mac = Mac.getInstance(hashingAlgorithmName, provider) - mac.init(secretKey) - val signAsByteArray = mac.doFinal(dataAsByteArray) - base64encoder.encode(signAsByteArray) + case _ => + false + } } - // TODO: Check implementation - // TODO: Check if serializer is needed - override def validate( - signature: String, + private def sign( data: String, - secret: String - )(implicit - serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder - ): Boolean = { - sign(data, secret) == signature + secret: Secret + )(implicit base64UrlEncoder: Base64UrlEncoder): JwsSignature = { + + val mac = Mac.getInstance(hashingAlgorithmName) + val key = new SecretKeySpec(secret.asByteArray, hashingAlgorithmName) + + mac.init(key) + + val signature = mac.doFinal(data.getBytes("UTF-8")) + + JwsSignature(base64UrlEncoder.encode(signature)) } } @@ -195,19 +162,19 @@ object Algorithm { secret: Secret )(implicit serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder + base64UrlEncoder: Base64UrlEncoder ): JwsSignature = { ??? } // TODO: Check implementation - override def sign( + def sign( data: String, secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder + base64UrlEncoder: Base64UrlEncoder ): String = { val key = getPrivateKey(secret) @@ -218,17 +185,17 @@ object Algorithm { signature.initSign(key) signature.update(dataByteArray) val signatureByteArray = signature.sign - base64encoder.encode(signatureByteArray) + base64UrlEncoder.encode(signatureByteArray) } // TODO: Check implementation - override def validate( + def validate( signature: String, data: String, secret: String )(implicit serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder + base64UrlEncoder: Base64UrlEncoder ): Boolean = { val key = getPublicKey(secret) @@ -238,7 +205,14 @@ object Algorithm { val rsaSignature = Signature.getInstance(hashingAlgorithmName, provider) rsaSignature.initVerify(key) rsaSignature.update(dataByteArray) - rsaSignature.verify(Base64Decoder.decode(signature)) + rsaSignature.verify(Base64UrlDecoder.decode(signature)) + } + + override def validate( + data: String, + secret: Secret + )(implicit base64encoder: Base64UrlEncoder): Boolean = { + ??? } private def getPublicKey(str: String): PublicKey = { @@ -290,4 +264,31 @@ object Algorithm { override protected def hashingAlgorithmName: String = "SHA512withRSA" } } + + def validate( + data: String, + secret: Secret // TODO: Do RSA algorithms use the same secret, probably not? + )(implicit + deserializeJson: String => JsonValue, + base64UrlDecoder: Base64UrlDecoder + ): Boolean = { + + data.split("\\.") match { + case Array( + base64UrlEncodedJoseHeader, + base64UrlEncodedJwsPayload, + base64EncodedSignature + ) => + val joseHeaderJson = deserializeJson { + base64UrlDecoder.decodeAsString { + base64UrlEncodedJoseHeader + } + } + + ??? // TODO: Continue here! + + case _ => + false + } + } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala deleted file mode 100644 index 85c111c..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64Decoder.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.janjaali.sprayjwt.encoder - -import java.util.Base64 - -/** - * Base64Decoder utility class. - */ -private[sprayjwt] object Base64Decoder { - - private lazy val base64Decoder: Base64.Decoder = Base64.getDecoder - - /** - * Decodes Base64 encoded text as ByteArray. - * - * @param text the text to decode as ByteArray - * @return Base64 decoded ByteArray - */ - def decode(text: String): Array[Byte] = { - base64Decoder.decode(text) - } - - /** - * Decodes Base64 decoded text as String. - * - * @param text the text to decode as String - * @return Base64 decoded String - */ - def decodeAsString(text: String): String = { - new String(decode(text)) - } - -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlDecoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlDecoder.scala new file mode 100644 index 0000000..43b96e1 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlDecoder.scala @@ -0,0 +1,33 @@ +package org.janjaali.sprayjwt.encoder + +import java.util.Base64 + +/** Default Base64 URL decoder that uses internally the [[java.util.Base64]] URL + * decoder. + */ +private[sprayjwt] trait Base64UrlDecoder { + + private lazy val decoder: Base64.Decoder = Base64.getUrlDecoder + + /** Decodes Base64 URL encoded text as ByteArray. + * + * @param text text that should be decoded + * @return Base64 URL decoded ByteArray + */ + def decode(text: String): Array[Byte] = { + decoder.decode(text) + } + + /** Decodes Base64 URL encoded text as String. + * + * @param text text that should be decoded + * @return Base64 URL decoded String + */ + def decodeAsString(text: String): String = { + new String(decode(text)) + } +} + +/** Base64 URL decoder utilities. + */ +private[sprayjwt] object Base64UrlDecoder extends Base64UrlDecoder diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala index edf5cf9..22da6e6 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala @@ -3,7 +3,7 @@ package org.janjaali.sprayjwt.encoder import java.util.Base64 /** Default Base64 URL encoder that uses internally the [[java.util.Base64]] URL - * encoder. + * encoder without padding. */ private[sprayjwt] trait Base64UrlEncoder { diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala index f6c631a..5ff9588 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/ByteEncoder.scala @@ -7,6 +7,8 @@ private[sprayjwt] object ByteEncoder { private val encodingCharset = "UTF-8" + // TODO: Check usage? + /** * Encodes text into a byte array used UTF-8 charset. * diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala index 931c883..b31d187 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -11,6 +11,7 @@ import org.janjaali.sprayjwt.json.JsonObject */ sealed abstract case class JoseHeader private (headers: Set[Header]) { + // TODO: Doc. def asJson: JsonObject = { JsonObject( headers.map { header => @@ -20,6 +21,7 @@ sealed abstract case class JoseHeader private (headers: Set[Header]) { } } +// TODO: Doc. object JoseHeader { /** Constructs a JOSE Header for a sequence of headers. diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala deleted file mode 100644 index dc1fcd3..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/JwtSpec.scala +++ /dev/null @@ -1,334 +0,0 @@ -package org.janjaali.sprayjwt - -import org.janjaali.sprayjwt.algorithms._ -import org.scalatest.funspec.AnyFunSpec -import spray.json.{JsBoolean, JsObject, JsString} - -class JwtSpec extends AnyFunSpec { - - describe("Jwt encodes the header to JSON") { - describe("HS256") { - val secret = "secret" - - it("encodes as JWT") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs256).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("encodes as JWT with iss") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt - .encode( - payload, - secret, - Algorithm.Hmac.Hs256, - JwtClaims(iss = Some("issuer")) - ) - .get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("encodes JsValue as JWT") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jsValue = JsObject( - "sub" -> JsString("1234567890"), - "name" -> JsString("John Doe"), - "admin" -> JsBoolean(true) - ) - - val jwt = LegacyJwt.encode(jsValue, secret, Algorithm.Hmac.Hs256).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("encodes JsValue as JWT with iss") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jsValue = JsObject( - "sub" -> JsString("1234567890"), - "name" -> JsString("John Doe"), - "admin" -> JsBoolean(true) - ) - - val jwt = LegacyJwt - .encode( - jsValue, - secret, - Algorithm.Hmac.Hs256, - JwtClaims(iss = Some("issuer")) - ) - .get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("encodes as JWT with all reserved claims") { - // scalastyle:off - val payload = - """{"sub":"1234567890","name":"John Doe","admin":true}""" - - val jwt = LegacyJwt - .encode( - payload, - secret, - Algorithm.Hmac.Hs256, - JwtClaims( - iss = Some("issuer"), - sub = Some("subject"), - aud = Some(Set("audience")), - isa = Some(500), - exp = Some(1000), - nbf = Some(2000), - iat = Some(3000), - jti = Some("jwtId") - ) - ) - .get - - val expectedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjIwMDAsImFkbWluIjp0cnVlLCJuYW1lIjoiSm9obiBEb2UiLCJqdGkiOiJqd3RJZCIsImV4cCI6MTAwMCwiaXNhIjo1MDAsImlhdCI6MzAwMCwic3ViIjoic3ViamVjdCIsImF1ZCI6ImF1ZGllbmNlIiwiaXNzIjoiaXNzdWVyIn0.2zS7vqKCLPKOlre6LYMMR/dTp41Q9jV5KiEyE9I6JLw" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - // scalastyle:off - val token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - // scalastyle:on - - val decodedPayload = LegacyJwt.decodeAsString(token, secret).get - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - - assert(decodedPayload == expected) - } - - it("decodes JWT as JsValue") { - // scalastyle:off - val token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - // scalastyle:on - - val json = LegacyJwt.decode(token, secret).get - - val expected = JsObject( - "sub" -> JsString("1234567890"), - "name" -> JsString("John Doe"), - "admin" -> JsBoolean(true) - ) - assert(json == expected) - } - - it("decodes JWT as JsValue with iss claim") { - // scalastyle:off - val token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imlzc3VlciJ9.MabLKH7FNuQXlshZe6m054If8TLP5DvwYccl0ejlUVA" - // scalastyle:on - - val json = LegacyJwt.decode(token, secret).get - - val expected = JsObject( - "sub" -> JsString("1234567890"), - "name" -> JsString("John Doe"), - "admin" -> JsBoolean(true), - "iss" -> JsString("issuer") - ) - assert(json == expected) - } - } - - describe("HS384") { - val secret = "secret" - - it("encodes as JWT") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs384).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - // scalastyle:off - val token = - "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" - // scalastyle:on - - val decodedPayload = LegacyJwt.decodeAsString(token, secret).get - - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - assert(decodedPayload == expected) - } - } - - describe("HS512") { - val secret = "secret" - - it("encodes as JWT") { - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Hmac.Hs512).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.YI0rUGDq5XdRw8vW2sDLRNFMN8Waol03iSFH8I4iLzuYK7FKHaQYWzPt0BJFGrAmKJ6SjY0mJIMZqNQJFVpkuw" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - // scalastyle:off - val token = - "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.DtVnCyiYCsCbg8gUP+579IC2GJ7P3CtFw6nfTTPw+0lZUzqgWAo9QIQElyxOpoRm" - // scalastyle:on - - val decodedPayload = LegacyJwt.decodeAsString(token, secret).get - - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - assert(decodedPayload == expected) - } - } - - describe("RS256") { - it("encodes as JWT") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = - try source.mkString - finally source.close - - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs256).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = - try source.mkString - finally source.close - - // scalastyle:off - val token = - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.CqkpMs2+ttnOcZMjCk3ca2Fw0d3yGBsS5X+eEtuPhYV77ApgRAidZqZvsC1Cs5hqhX6ZTuer0UnCAQ5n4gvyLoaMiMiGqtm+UeHiUKQSeThtqf4M5ylMERi971gZV5ffXPeAHUZUPN8IiMof2BjUwOk4cN7WVfz5i80zcXAkbBUcra2uPlvVpHXGrIVI3CPpBYs4Hn3towNHX9bpWnqfvogy5TXzMEVHAF8H/TgGDwmCMuIGmi4xdlVviXTXrF/znPNNowTuI8aaXenJRYaDkI0VyN6MChmsA8aDOMMSlikDrgGzdxQSGJrBSrvrjnuJMK9raJ7dr/1U+5Rghtms+Q" - // scalastyle:on - - val decoded = LegacyJwt.decodeAsString(token, public).get - - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - assert(decoded == expected) - } - } - - describe("RS384") { - it("encodes as JWT") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = - try source.mkString - finally source.close - - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs384).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = - try source.mkString - finally source.close - - // scalastyle:off - val token = - "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.oCcq/xI4jcjxMB5zRSp6F7bfQjT2KhdH4fJkN3E24wa6ltE2UufgXQ4/wJjOalJ4h0RbnEUwlMzdD3hJgNRU7BfD6r5GzVo/RLTLTkyTD+KsHXYiS4qHYOZ1otyoPFV/QzQcovoOXT+kmsVH/S6mpVzN1Qh1OUgu+2D9swH+6rZi0YctrKv3dXou+GSVt1l5xfyA7R4KB8HwONTwdyEbTSM/aJWP+Ob80kDNAEs9xkx/2KzY1iGfdh0FwIU2OKdc+b0CNlVQrbYwLX55Yk5CBPJY6UNlXwSmCFwlbZvjChkwE5MH4ICDLWN7j2llm6PX38RE2xRCguqId/iA8vwj8g" - // scalastyle:on - - val decoded = LegacyJwt.decodeAsString(token, public).get - - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - assert(decoded == expected) - } - } - - describe("RS512") { - it("encodes as JWT") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.private.key")) - val secret = - try source.mkString - finally source.close - - val payload = """{"sub":"1234567890","name":"John Doe","admin":true}""" - val jwt = LegacyJwt.encode(payload, secret, Algorithm.Rsa.Rs512).get - - // scalastyle:off - val expectedJwt = - "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" - // scalastyle:on - - assert(jwt == expectedJwt) - } - - it("decodes JWT as String") { - val source = - scala.io.Source.fromURL(getClass.getResource("test.rsa.public.key")) - val public = - try source.mkString - finally source.close - - // scalastyle:off - val token = - "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.JuhmKgWohwJS9lLhBo5ealB+RA5CX6VgzYExvUKKL/v5oqEO1bZ91Ayi949jvB5tTgvMOX3njfKpl8tyazfqoHjsXzRvHX/NdUGx1rWhWZ826Zdpgm32fO15Jv1xHbxWbFaqp0zwyLUKPo756lmg1+8IeTBdDvhC7XSlBc9cUDe4x3anltjeUseZllS2PZgQn0pxYXK5KVbAsIasDthprmaJheLBgO+CInCpDiVukUC2WfCGz9tr9IhKwNgLPkcue4uVRubOgV8By68SMZgVdxZXP70siV/sMOqrILyWk7Zi0fSm/JC4QP4fZSenfxwl8FEr4Rs+FWL/clk3fnMYQA" - // scalastyle:on - - val decoded = LegacyJwt.decodeAsString(token, public).get - - val expected = """{"sub":"1234567890","name":"John Doe","admin":true}""" - assert(decoded == expected) - } - } - } - -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala index 26b73f6..a1d7ee8 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala @@ -1,60 +1,38 @@ package org.janjaali.sprayjwt.algorithms import org.janjaali.sprayjwt.encoder.Base64UrlEncoder -import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ -import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} -import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} import org.janjaali.sprayjwt.tests.ScalaTestSpec -trait AlgorithmSpec extends ScalaTestSpec { +final class AlgorithmSpec extends ScalaTestSpec { - implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder + private implicit val base64UrlEncoder = Base64UrlEncoder - protected def verifyWithHmac256Algorithm(implicit - serializeJson: JsonValue => String - ): Unit = { + "Algorithm" - { - verifyWithAlgorithm( - algorithm = Algorithm.Hmac.Hs256, - expectedSignature = JwsSignature( - "jUzTJEnlFeTXDUPp9vJMwoalvXJ55IZ6DaBExN08UtA" - ) - ) - } + "should validate raw JWT string representation" - { + + "when signed with" - { + + "HS256." in { + + Algorithm.Hmac.Hs256.validate( + data = { + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w", + }, + secret = Secret("secret value") + ) shouldBe false + } + + "Hs512." in { - private def verifyWithAlgorithm( - algorithm: Algorithm, - expectedSignature: JwsSignature - )(implicit - serializeJson: JsonValue => String - ): Unit = { - - "Verify serializer with HS256 algorithm." in { - - val joseHeader = JoseHeader( - Seq( - Header.Type(Header.Type.Value.Jwt), - // TODO: That's weird, how can we set the algorithm and then just - // sign with another one? - Header.Algorithm(algorithm) - ) - ) - - val jwsPayload = JwsPayload( - JwtClaimsSet( - Seq( - // TODO: Maybe add an iss type? - Claim.Private(name = "iss", value = "joe"), - Claim.ExpirationTime(NumericDate(1300819380L)), - Claim.Private(name = "http://example.com/is_root", value = true) - ) - ) - ) - - val secret = Secret("secret value") - - algorithm.sign(joseHeader, jwsPayload, secret) shouldBe expectedSignature + Algorithm.Hmac.Hs512.validate( + data = { + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w", + }, + secret = Secret("secret value") + ) shouldBe false + } + } } } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala new file mode 100644 index 0000000..123c3f7 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala @@ -0,0 +1,82 @@ +package org.janjaali.sprayjwt.algorithms + +import org.janjaali.sprayjwt.encoder.Base64UrlEncoder +import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ +import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} +import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} +import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +trait JsonStringSerializerSpec extends ScalaTestSpec { + + private implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder + + protected def jsonStringSerializer: JsonStringSerializer + + protected def verifySignWithHmac256Algorithm(): Unit = { + + verifySignWithAlgorithm( + algorithm = Algorithm.Hmac.Hs256, + expectedSignature = JwsSignature( + "jUzTJEnlFeTXDUPp9vJMwoalvXJ55IZ6DaBExN08UtA" + ) + ) + } + + protected def verifySignWithHmac384Algorithm(): Unit = { + + verifySignWithAlgorithm( + algorithm = Algorithm.Hmac.Hs384, + expectedSignature = JwsSignature( + "tz6NV8IfhPNqEnfUgeu0TJowwvWsjcmFCiRC_F-7bTOQeUle8jomj151nYHx1-IQ" + ) + ) + } + + protected def verifySignWithHmac512Algorithm(): Unit = { + + verifySignWithAlgorithm( + algorithm = Algorithm.Hmac.Hs512, + expectedSignature = JwsSignature( + "dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w" + ) + ) + } + + private def verifySignWithAlgorithm( + algorithm: Algorithm, + expectedSignature: JwsSignature + ): Unit = { + + s"Verify serializer with algorithm '${algorithm}'." in { + + val joseHeader = JoseHeader( + Seq( + Header.Type(Header.Type.Value.Jwt), + // TODO: That's weird, how can we set the algorithm and then just + // sign with another one? + Header.Algorithm(algorithm) + ) + ) + + val jwsPayload = JwsPayload( + JwtClaimsSet( + Seq( + // TODO: Maybe add an iss type? + Claim.Private(name = "iss", value = "joe"), + Claim.ExpirationTime(NumericDate(1300819380L)), + Claim.Private(name = "http://example.com/is_root", value = true) + ) + ) + ) + + val secret = Secret("secret value") + + algorithm.sign(joseHeader, jwsPayload, secret) shouldBe expectedSignature + } + } + + private implicit def serializeJson: JsonValue => String = { + jsonStringSerializer.serialize + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala index 9c558e1..6609daa 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/encoder/Base64DecoderSpec.scala @@ -6,7 +6,7 @@ class Base64DecoderSpec extends AnyFunSpec { describe("Base64Decoder") { it("decodes text as Base64 decoded byte-array") { - val decodedByteArray = Base64Decoder.decode("ZGFuY2U=") + val decodedByteArray = Base64UrlDecoder.decode("ZGFuY2U=") assert(decodedByteArray sameElements "dance".getBytes) } } From df0a7bf32ce821796ce1224582e29b56891159c5 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Mon, 26 Apr 2021 22:53:14 +0200 Subject: [PATCH 34/43] WIP --- .../sprayjwt/algorithms/Algorithm.scala | 16 ++- .../sprayjwt/json/CommonJsonWriters.scala | 18 +++ .../json/JsonStringDeserializer.scala | 13 ++ .../sprayjwt/json/JsonStringSerializer.scala | 4 +- .../org/janjaali/sprayjwt/jws/Header.scala | 77 ++++++++--- .../janjaali/sprayjwt/jws/JoseHeader.scala | 19 +++ .../janjaali/sprayjwt/jws/HeaderSpec.scala | 120 +++++++++++++++++- .../sprayjwt/jws/JoseHeaderSpec.scala | 81 +++++++++++- .../tests/ScalaCheckGeneratorsSampler.scala | 2 + 9 files changed, 320 insertions(+), 30 deletions(-) create mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index cd419a8..56e65a9 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -15,6 +15,11 @@ import org.janjaali.sprayjwt.jws.{JoseHeader, JwsPayload, JwsSignature} import java.io.{IOException, StringReader} import java.security.{PrivateKey, PublicKey, Signature} +import org.janjaali.sprayjwt.json.JsonObject +import org.janjaali.sprayjwt.json.JsonString +import org.janjaali.sprayjwt.json.JsonNumber +import org.janjaali.sprayjwt.json.JsonBoolean +import org.janjaali.sprayjwt.jws.Header /** Represents a cryptographic algorithm used with JWT. */ @@ -285,10 +290,13 @@ object Algorithm { } } - ??? // TODO: Continue here! - - case _ => - false + joseHeaderJson match { + case joseHeaderJson: JsonObject => + val joseHeader = JoseHeader(joseHeaderJson) + ??? // TODO: Continue here + case _ => false + } + case _ => false } } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala index c51911c..c67c2ab 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/CommonJsonWriters.scala @@ -39,6 +39,14 @@ object CommonJsonWriters { implicit def stringJsonWriter: JsonWriter[String] = { CommonJsonWriters.stringJsonWriter } + + /** Constructs JSON writer for JSON values. + * + * @return JSON writer + */ + implicit def jsonValueJsonWriter: JsonWriter[JsonValue] = { + CommonJsonWriters.jsonValueJsonWriter + } } /** Constructs JSON writer for Int types. @@ -103,4 +111,14 @@ object CommonJsonWriters { } } } + + /** Constructs JSON writer for JSON values. + * + * @return JSON writer + */ + def jsonValueJsonWriter = { + new JsonWriter[JsonValue] { + override def write(value: JsonValue): JsonValue = value + } + } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala new file mode 100644 index 0000000..6dab137 --- /dev/null +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala @@ -0,0 +1,13 @@ +package org.janjaali.sprayjwt.json + +// TODO: Docs. + +trait JsonStringDeserializer { + + final object Implicits { + + implicit val implicitDeserialize: String => JsonValue = deserialize + } + + def deserialize(jsonText: String): JsonValue +} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala index c946636..a1def4c 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -6,9 +6,7 @@ trait JsonStringSerializer { final object Implicits { - implicit def implicitSerialize(json: JsonValue): String = { - serialize(json) - } + implicit val implicitSerialize: JsonValue => String = serialize } def serialize(json: JsonValue): String diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala index c88d8c4..c636485 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala @@ -1,7 +1,7 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.algorithms -import org.janjaali.sprayjwt.json.{JsonString, JsonValue, JsonWriter} +import org.janjaali.sprayjwt.json._ /** Represents a Header. */ @@ -48,15 +48,21 @@ object Header { value: algorithms.Algorithm ) extends Header { - override def name: String = "alg" + override def name: String = Algorithm.name override type T = algorithms.Algorithm - override protected def valueJsonWriter: JsonWriter[ - algorithms.Algorithm - ] = { - new JsonWriter[algorithms.Algorithm] { + override protected def valueJsonWriter: JsonWriter[algorithms.Algorithm] = { + Algorithm.valueJsonWriter + } + } + + object Algorithm { + + val name: String = "alg" + private def valueJsonWriter: JsonWriter[algorithms.Algorithm] = { + new JsonWriter[algorithms.Algorithm] { override def write(algorithm: algorithms.Algorithm): JsonValue = { algorithm match { case algorithms.Algorithm.Rsa.Rs256 => JsonString("RS256") @@ -69,6 +75,18 @@ object Header { } } } + + private[Header] def apply(algorithmName: String): Option[Algorithm] = { + algorithmName match { + case "RS256" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs256)) + case "RS384" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs384)) + case "RS512" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs512)) + case "HS256" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs256)) + case "HS384" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs384)) + case "HS512" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs512)) + case _ => None + } + } } /** Declares the media type of the complete JWS. @@ -79,19 +97,12 @@ object Header { value: Type.Value ) extends Header { - override def name: String = "typ" + override def name: String = Type.name override type T = Type.Value override protected def valueJsonWriter: JsonWriter[Type.Value] = { - new JsonWriter[Type.Value] { - - override def write(value: Type.Value): JsonValue = { - value match { - case Type.Value.Jwt => JsonString("JWT") - } - } - } + Type.valueJsonWriter } } @@ -99,6 +110,8 @@ object Header { */ object Type { + val name: String = "typ" + sealed trait Value object Value { @@ -107,6 +120,23 @@ object Header { */ case object Jwt extends Value } + + private def valueJsonWriter: JsonWriter[Type.Value] = { + new JsonWriter[Type.Value] { + override def write(value: Type.Value): JsonValue = { + value match { + case Type.Value.Jwt => JsonString("JWT") + } + } + } + } + + private[Header] def apply(typeValue: String): Option[Type] = { + typeValue match { + case "JWT" => Some(Type(Type.Value.Jwt)) + case _ => None + } + } } /** Represents a private header that producer and consumer of a JWT can @@ -122,4 +152,21 @@ object Header { implicitly[JsonWriter[T]] } } + + // TODO: Add docs. + + def apply(name: String, value: JsonValue): Header = { + + import CommonJsonWriters.Implicits.jsonValueJsonWriter + + (name, value) match { + case (Algorithm.name, JsonString(algorithmName)) + if Algorithm(algorithmName).isDefined => + Algorithm(algorithmName).get + case (Type.name, JsonString(typeValue)) if Type(typeValue).isDefined => + Type(typeValue).get + case (name, value) => + Private(name, value) + } + } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala index b31d187..5bb4698 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/JoseHeader.scala @@ -2,6 +2,10 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.util.CollectionsFactory import org.janjaali.sprayjwt.json.JsonObject +import org.janjaali.sprayjwt.json.JsonValue +import org.janjaali.sprayjwt.json.JsonString +import org.janjaali.sprayjwt.json.JsonNumber +import org.janjaali.sprayjwt.json.JsonBoolean /** Javascript Object Signing and Encryption (JOSE Header) that contains * the parameter that describes the cryptographic operations and @@ -42,4 +46,19 @@ object JoseHeader { new JoseHeader(uniquelyNamedHeaders.toSet) {} } + + def apply( + json: JsonObject + ): JoseHeader = { + + val headers = { + json.members.map { case (name, value) => + Header(name, value) + }.toList + } + + JoseHeader(headers) + } + + sealed trait DeserializationFailure } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala index ae7c8b2..499c445 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala @@ -1,7 +1,11 @@ package org.janjaali.sprayjwt.jws -import org.janjaali.sprayjwt.json.JsonString +import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits.jsonValueJsonWriter +import org.janjaali.sprayjwt.json.{JsonBoolean, JsonString, JsonValue} +import org.janjaali.sprayjwt.tests.ScalaCheckGeneratorsSampler._ import org.janjaali.sprayjwt.tests.ScalaTestSpec +import org.scalacheck.Gen import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { @@ -31,4 +35,118 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { } } } + + "Header" - { + + "when constructed from a header name and value" - { + + "when header name is 'alg'" - { + + val headerName = "alg" + + "when value matches an 'RS256'" - { + + behave like createAlgorithmHeader( + value = JsonString("RS256"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs256) + ) + } + + "when value matches an 'RS384'" - { + + behave like createAlgorithmHeader( + value = JsonString("RS384"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs384) + ) + } + + "when value matches an 'RS512'" - { + + behave like createAlgorithmHeader( + value = JsonString("RS512"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs512) + ) + } + + "when value matches an 'HS256'" - { + + behave like createAlgorithmHeader( + value = JsonString("HS256"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs256) + ) + } + + "when value matches an 'HS384'" - { + + behave like createAlgorithmHeader( + value = JsonString("HS384"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs384) + ) + } + + "when value matches an 'HS512'" - { + + behave like createAlgorithmHeader( + value = JsonString("HS512"), + expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs512) + ) + } + + def createAlgorithmHeader( + value: JsonString, + expectedHeader: Header.Algorithm + ): Unit = { + + s"should create $expectedHeader." in { + Header(headerName, value) shouldBe expectedHeader + } + } + + "when value does not match an algorithm" - { + + "should create a private header." in { + + Header(headerName, JsonBoolean(false)) shouldBe { + Header.Private[JsonValue](headerName, JsonBoolean(false)) + } + } + } + } + + "when header name is 'typ'" - { + + val headerName = "typ" + + "when value matches a type value" - { + + "should create a type header." in { + + Header(headerName, JsonString("JWT")) shouldBe { + Header.Type(Header.Type.Value.Jwt) + } + } + } + + "when value does not match a type value" - { + + "should create a private header." in { + + Header(headerName, JsonBoolean(false)) shouldBe { + Header.Private[JsonValue](headerName, JsonBoolean(false)) + } + } + } + } + + "when header name does not match 'alg' nor 'typ'" - { + + "should create a private header." in { + + Header("arbitrary", JsonBoolean(false)) shouldBe { + Header.Private[JsonValue]("arbitrary", JsonBoolean(false)) + } + } + } + } + } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala index ac31a8d..4d5c255 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/JoseHeaderSpec.scala @@ -1,23 +1,22 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.algorithms.Algorithm -import org.janjaali.sprayjwt.json.JsonObject +import org.janjaali.sprayjwt.json.{JsonNumber, JsonObject} import org.janjaali.sprayjwt.tests.{ScalaCheckGeneratorsSampler, ScalaTestSpec} import org.scalacheck.Gen import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import org.janjaali.sprayjwt.json.JsonValue class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { "Jose Header" - { - "when constructed" - { - - val sut = JoseHeader.apply _ + "when constructed from a sequence of headers" - { "should not contain headers with the same name." in { forAll(ScalaCheckGenerators.headersGen) { headers => - val headerNames = sut(headers).headers.map(_.name) + val headerNames = JoseHeader(headers).headers.map(_.name) headerNames.toSet.size shouldBe headerNames.size } @@ -30,7 +29,7 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { headers.groupBy(_.name).values.map(_.head).toSeq } - val joseHeader = sut(distinctNamedHeaders) + val joseHeader = JoseHeader(distinctNamedHeaders) joseHeader.headers should contain theSameElementsAs { distinctNamedHeaders @@ -44,7 +43,7 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ - val joseHeaders = sut( + val joseHeaders = JoseHeader( List(Header.Private("name", 1), Header.Private("name", 3)) ) @@ -53,6 +52,74 @@ class JoseHeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { } } + "when constructed from a JSON object" - { + + "should not contain headers with the same name." in { + + forAll(ScalaCheckGenerators.headersGen) { headers => + + val json = { + JsonObject( + headers.map { header => + header.name -> header.valueAsJson + }.toMap + ) + } + + val headerNames = JoseHeader(json).headers.map(_.name) + + headerNames.toSet.size shouldBe headerNames.size + } + } + + "should add headers with distinct names." in { + + forAll(ScalaCheckGenerators.headersGen) { headers => + val distinctNamedHeaders = { + headers.groupBy(_.name).values.map(_.head).toSeq + } + + val json = { + JsonObject( + distinctNamedHeaders.map { header => + header.name -> header.valueAsJson + }.toMap + ) + } + + val joseHeader = JoseHeader(json) + + joseHeader.headers.map(_.name) should contain theSameElementsAs { + distinctNamedHeaders.map(_.name) + } + } + } + + "with headers with the same name" - { + + "should add the later ones." in { + + import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ + + val joseHeaders = JoseHeader( + JsonObject( + Map( + "name" -> JsonNumber(1), + "name" -> JsonNumber(3) + ) + ) + ) + + joseHeaders.headers should contain only { + Header.Private[JsonValue]( + "name", + JsonNumber(3) + ) // TODO: JsonNumber(3) should be 3 - grant implicit to deserialize such JSON values in private headers + } + } + } + } + "when serialized as JSON" - { "should result in a JSON object." in { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala index 8bbeba3..2b69c23 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaCheckGeneratorsSampler.scala @@ -19,3 +19,5 @@ trait ScalaCheckGeneratorsSampler { } } } + +object ScalaCheckGeneratorsSampler extends ScalaCheckGeneratorsSampler From 7dc93aedc73d21699191ba08bccddbd6d38af912 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Fri, 7 May 2021 21:26:20 +0200 Subject: [PATCH 35/43] WIP. --- .../SprayJsonStringDeserializer.scala | 36 ++++++++++ .../sprayjson/SprayJsonStringSerializer.scala | 2 + .../SprayJsonStringDeserializerSpec.scala | 16 +++++ .../SprayJsonStringSerializerSpec.scala | 9 +-- .../sprayjwt/algorithms/Algorithm.scala | 57 ++++++++++----- .../janjaali/sprayjwt/json/JsonValue.scala | 2 + .../sprayjwt/algorithms/AlgorithmSpec.scala | 38 ---------- .../JsonStringDeserializerSpec.scala | 72 +++++++++++++++++++ .../algorithms/JsonStringSerializerSpec.scala | 15 +++- 9 files changed, 180 insertions(+), 67 deletions(-) create mode 100644 spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala create mode 100644 spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala create mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala new file mode 100644 index 0000000..d6f8c1e --- /dev/null +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala @@ -0,0 +1,36 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.json._ +import spray.json._ + +object SprayJsonStringDeserializer extends JsonStringDeserializer { + + override def deserialize(jsonText: String): JsonValue = { + asJsonValue(jsonText.parseJson) + } + + private def asJsonValue(jsValue: JsValue): JsonValue = { + jsValue match { + + case JsObject(fields) => + JsonObject( + fields.map { case (name, jsValue) => + name -> asJsonValue(jsValue) + } + ) + + case JsArray(elements) => + ??? + + case JsString(value) => + JsonString(value) + + case JsNumber(value) => + JsonNumber(value) + + case jsBoolean: JsBoolean => JsonBoolean(jsBoolean.value) + + case JsNull => JsonNull + } + } +} diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala index 2c01e58..2699712 100644 --- a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala @@ -23,6 +23,8 @@ object SprayJsonStringSerializer extends JsonStringSerializer { JsNumber(value) case JsonBoolean(value) => JsBoolean(value) + case JsonNull => + JsNull } } } diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala new file mode 100644 index 0000000..a16cfd9 --- /dev/null +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala @@ -0,0 +1,16 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.algorithms.JsonStringDeserializerSpec +import org.janjaali.sprayjwt.json.JsonStringDeserializer + +final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec { + + override protected def jsonStringDeserializer: JsonStringDeserializer = { + SprayJsonStringDeserializer + } + + "SprayJsonStringDeserializer" - { + + verifyValidationWithHmacAlgorithms() + } +} diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala index b705ccf..9d656ff 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala @@ -1,8 +1,7 @@ package org.janjaali.sprayjwt.sprayjson import org.janjaali.sprayjwt.algorithms.{Algorithm, JsonStringSerializerSpec} -import org.janjaali.sprayjwt.json.JsonValue -import org.janjaali.sprayjwt.json.JsonStringSerializer +import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec { @@ -12,10 +11,6 @@ final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec { "SprayJsonStringSerializer" - { - verifySignWithHmac256Algorithm() - - verifySignWithHmac384Algorithm() - - verifySignWithHmac512Algorithm() + verifySignWithHmacAlgorithms() } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 56e65a9..ee0bfab 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -270,33 +270,52 @@ object Algorithm { } } + // TODO: Docs. + // TODO: Test. def validate( data: String, - secret: Secret // TODO: Do RSA algorithms use the same secret, probably not? + secret: Secret // TODO: Do RSA algorithms use the same kind of secret, probably not? )(implicit deserializeJson: String => JsonValue, - base64UrlDecoder: Base64UrlDecoder + base64UrlDecoder: Base64UrlDecoder, + base64UrlEncoder: Base64UrlEncoder ): Boolean = { - data.split("\\.") match { - case Array( - base64UrlEncodedJoseHeader, - base64UrlEncodedJwsPayload, - base64EncodedSignature - ) => - val joseHeaderJson = deserializeJson { - base64UrlDecoder.decodeAsString { - base64UrlEncodedJoseHeader + val maybeAlgorithm = { + data.split("\\.") match { + case Array( + base64UrlEncodedJoseHeader, + base64UrlEncodedJwsPayload, + base64EncodedSignature + ) => + val maybeJoseHeaderJsonObject = deserializeJson { + base64UrlDecoder.decodeAsString { + base64UrlEncodedJoseHeader + } } - } - joseHeaderJson match { - case joseHeaderJson: JsonObject => - val joseHeader = JoseHeader(joseHeaderJson) - ??? // TODO: Continue here - case _ => false - } - case _ => false + maybeJoseHeaderJsonObject match { + case joseHeaderJsonObject: JsonObject => + val joseHeader = JoseHeader(joseHeaderJsonObject) + + joseHeader.headers.collectFirst { + case Header.Algorithm(algorithm) => algorithm + } + case _ => + // TODO: Log. + None + } + case _ => + // TODO: Log. + None + } + } + + maybeAlgorithm match { + case Some(algorithm) => + algorithm.validate(data, secret) + case None => + false } } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 2a5d5b3..398a406 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -12,3 +12,5 @@ final case class JsonString(value: String) extends JsonValue final case class JsonNumber(value: BigDecimal) extends JsonValue final case class JsonBoolean(value: Boolean) extends JsonValue + +final case object JsonNull extends JsonValue diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala deleted file mode 100644 index a1d7ee8..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/AlgorithmSpec.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -import org.janjaali.sprayjwt.encoder.Base64UrlEncoder -import org.janjaali.sprayjwt.tests.ScalaTestSpec - -final class AlgorithmSpec extends ScalaTestSpec { - - private implicit val base64UrlEncoder = Base64UrlEncoder - - "Algorithm" - { - - "should validate raw JWT string representation" - { - - "when signed with" - { - - "HS256." in { - - Algorithm.Hmac.Hs256.validate( - data = { - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w", - }, - secret = Secret("secret value") - ) shouldBe false - } - - "Hs512." in { - - Algorithm.Hmac.Hs512.validate( - data = { - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w", - }, - secret = Secret("secret value") - ) shouldBe false - } - } - } - } -} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala new file mode 100644 index 0000000..eea7041 --- /dev/null +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala @@ -0,0 +1,72 @@ +package org.janjaali.sprayjwt.algorithms + +import org.janjaali.sprayjwt.encoder.{Base64UrlDecoder, Base64UrlEncoder} +import org.janjaali.sprayjwt.json.{JsonStringDeserializer, JsonValue} +import org.janjaali.sprayjwt.tests.ScalaTestSpec + +trait JsonStringDeserializerSpec extends ScalaTestSpec { + + private implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder + + private implicit val base64UrlDecoder: Base64UrlDecoder = Base64UrlDecoder + + protected def jsonStringDeserializer: JsonStringDeserializer + + protected def verifyValidationWithHmacAlgorithms(): Unit = { + + verifyValidationWithHmac256Algorithm() + + verifyValidationWithHmac384Algorithm() + + verifyValidationWithHmac512Algorithm() + } + + private def verifyValidationWithHmac256Algorithm(): Unit = { + + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac256", + data = { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.eMS0OOcs-0Eo5x5vDepYOcITeG4NtPrtE8seTsT1RT0" + }, + secret = Secret("secret value") + ) + } + + private def verifyValidationWithHmac384Algorithm(): Unit = { + + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac384", + data = { + "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tXvOFupOdp6ISki7ysqn_gO9LHk2n35d0f_E26d9FYfhLLHNudoWU2HXD_6Tnm7X" + }, + secret = Secret("secret value") + ) + } + + private def verifyValidationWithHmac512Algorithm(): Unit = { + + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac512", + data = { + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.mV13yTaWA-6cQWcQEmUOh2qaTnpF5hQpNNkIMY6RIlbyQQQ-MbE9zjQ19dTQpQ2hxEql5ObbmDmWzqx13ka6Iw", + }, + secret = Secret("secret value") + ) + } + + private def verifyValidationWithHmacAlgorithm( + algorithmName: String, + data: String, + secret: Secret + ): Unit = { + + s"Verify deserializer with '$algorithmName'." in { + + Algorithm.validate(data, secret) shouldBe true + } + } + + private implicit def serializeJson: String => JsonValue = { + jsonStringDeserializer.deserialize + } +} diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala index 123c3f7..7ad8f64 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala @@ -13,7 +13,16 @@ trait JsonStringSerializerSpec extends ScalaTestSpec { protected def jsonStringSerializer: JsonStringSerializer - protected def verifySignWithHmac256Algorithm(): Unit = { + protected def verifySignWithHmacAlgorithms(): Unit = { + + verifySignWithHmac256Algorithm() + + verifySignWithHmac384Algorithm() + + verifySignWithHmac512Algorithm() + } + + private def verifySignWithHmac256Algorithm(): Unit = { verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs256, @@ -23,7 +32,7 @@ trait JsonStringSerializerSpec extends ScalaTestSpec { ) } - protected def verifySignWithHmac384Algorithm(): Unit = { + private def verifySignWithHmac384Algorithm(): Unit = { verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs384, @@ -33,7 +42,7 @@ trait JsonStringSerializerSpec extends ScalaTestSpec { ) } - protected def verifySignWithHmac512Algorithm(): Unit = { + private def verifySignWithHmac512Algorithm(): Unit = { verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs512, From e7813445c7abd564addc7806099fe12eb254db4f Mon Sep 17 00:00:00 2001 From: Janjaali Date: Fri, 7 May 2021 21:30:18 +0200 Subject: [PATCH 36/43] WIP. --- .../org/janjaali/sprayjwt/algorithms/Algorithm.scala | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index ee0bfab..83aae84 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -10,16 +10,11 @@ import org.janjaali.sprayjwt.encoder.{ Base64UrlEncoder, ByteEncoder } -import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -import org.janjaali.sprayjwt.jws.{JoseHeader, JwsPayload, JwsSignature} +import org.janjaali.sprayjwt.json._ +import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} import java.io.{IOException, StringReader} import java.security.{PrivateKey, PublicKey, Signature} -import org.janjaali.sprayjwt.json.JsonObject -import org.janjaali.sprayjwt.json.JsonString -import org.janjaali.sprayjwt.json.JsonNumber -import org.janjaali.sprayjwt.json.JsonBoolean -import org.janjaali.sprayjwt.jws.Header /** Represents a cryptographic algorithm used with JWT. */ From 0b48aff2a07274fde05af0499935333cbd930b46 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sun, 9 May 2021 13:26:41 +0200 Subject: [PATCH 37/43] WIP. --- .scalafmt.conf | 4 +++- build.sbt | 24 ++++++++++--------- project/build.properties | 2 +- project/plugins.sbt | 1 - .../SprayJsonStringDeserializer.scala | 13 ++++------ .../sprayjson/SprayJsonStringSerializer.scala | 12 ++++------ .../SprayJsonStringDeserializerSpec.scala | 6 ++--- .../SprayJsonStringSerializerSpec.scala | 6 ++--- 8 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 project/plugins.sbt diff --git a/.scalafmt.conf b/.scalafmt.conf index da3b0d4..e46ff79 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,6 @@ -version = "2.7.5" +version = "3.0.0-RC2" newlines.topLevelStatements = [before] newlines.afterCurlyLambdaParams = preserve + +runner.dialect = scala3 diff --git a/build.sbt b/build.sbt index 32c5a5b..e58244b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.13.4" +ThisBuild / scalaVersion := "3.0.0-RC3" ThisBuild / versionScheme := Some("early-semver") @@ -7,26 +7,27 @@ lazy val sprayJwt = (project in file("spray-jwt")) name := "spray-jwt", organization := "com.github.janjaali", version := "1.0.0", + licenses := Seq( "MIT License" -> url("https://opensource.org/licenses/MIT") ), homepage := Some(url("https://github.com/janjaali/spray-jwt")), scmInfo := Some( ScmInfo( - url("https://github.com/janjaali/spray-jwt"), - "scm:git@github.com/janjaali/spray-jwt.git" + browseUrl = url("https://github.com/janjaali/spray-jwt"), + connection = "scm:git@github.com/janjaali/spray-jwt.git" ) ), developers := List( Developer( - id = "ghashange", - name = "ghashange", + id = "janjaali", + name = "janjaali", email = "", url = url("https://github.com/janjaali") ) ), + publishMavenStyle := true, - publishArtifact in Test := false, publishTo := { val nexus = "https://oss.sonatype.org/" if (isSnapshot.value) { @@ -35,16 +36,17 @@ lazy val sprayJwt = (project in file("spray-jwt")) Some("releases" at nexus + "service/local/staging/deploy/maven2") } }, + libraryDependencies ++= Seq( // JSON - "io.spray" %% "spray-json" % "1.3.6", + ("io.spray" %% "spray-json" % "1.3.6").cross(CrossVersion.for3Use2_13), // Encryption "org.bouncycastle" % "bcpkix-jdk15on" % "1.58", // Test - "org.scalatest" %% "scalatest" % "3.2.6" % Test, + ("org.scalatest" %% "scalatest" % "3.2.7" % Test).cross(CrossVersion.for3Use2_13), // Property based tests - "org.scalacheck" %% "scalacheck" % "1.15.3" % Test, - "org.scalatestplus" %% "scalacheck-1-15" % "3.2.6.0" % Test + ("org.scalacheck" %% "scalacheck" % "1.15.3" % Test).cross(CrossVersion.for3Use2_13), + ("org.scalatestplus" %% "scalacheck-1-15" % "3.2.6.0" % Test).cross(CrossVersion.for3Use2_13) ) ) @@ -53,6 +55,6 @@ lazy val sprayJsonSupport = (project in file("spray-json-support")) .settings { libraryDependencies ++= Seq( // Supported JSON library - "io.spray" %% "spray-json" % "1.3.6" + ("io.spray" %% "spray-json" % "1.3.6").cross(CrossVersion.for3Use2_13) ) } diff --git a/project/build.properties b/project/build.properties index b366316..7bb94aa 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.5.0 +sbt.version = 1.5.1 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index 4c1476e..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala index d6f8c1e..5a1749e 100644 --- a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala @@ -3,15 +3,13 @@ package org.janjaali.sprayjwt.sprayjson import org.janjaali.sprayjwt.json._ import spray.json._ -object SprayJsonStringDeserializer extends JsonStringDeserializer { +object SprayJsonStringDeserializer extends JsonStringDeserializer: - override def deserialize(jsonText: String): JsonValue = { + override def deserialize(jsonText: String): JsonValue = asJsonValue(jsonText.parseJson) - } - - private def asJsonValue(jsValue: JsValue): JsonValue = { - jsValue match { + private def asJsonValue(jsValue: JsValue): JsonValue = + jsValue match case JsObject(fields) => JsonObject( fields.map { case (name, jsValue) => @@ -31,6 +29,3 @@ object SprayJsonStringDeserializer extends JsonStringDeserializer { case jsBoolean: JsBoolean => JsonBoolean(jsBoolean.value) case JsNull => JsonNull - } - } -} diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala index 2699712..7311334 100644 --- a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala @@ -3,14 +3,13 @@ package org.janjaali.sprayjwt.sprayjson import org.janjaali.sprayjwt.json._ import spray.json._ -object SprayJsonStringSerializer extends JsonStringSerializer { +object SprayJsonStringSerializer extends JsonStringSerializer: - override def serialize(jsonValue: JsonValue): String = { + override def serialize(jsonValue: JsonValue): String = sprayJsonValue(jsonValue).compactPrint - } - private def sprayJsonValue(jsonValue: JsonValue): JsValue = { - jsonValue match { + private def sprayJsonValue(jsonValue: JsonValue): JsValue = + jsonValue match case JsonObject(members) => JsObject( members.map { case (key, value) => @@ -25,6 +24,3 @@ object SprayJsonStringSerializer extends JsonStringSerializer { JsBoolean(value) case JsonNull => JsNull - } - } -} diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala index a16cfd9..f07a00e 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala @@ -3,14 +3,12 @@ package org.janjaali.sprayjwt.sprayjson import org.janjaali.sprayjwt.algorithms.JsonStringDeserializerSpec import org.janjaali.sprayjwt.json.JsonStringDeserializer -final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec { +final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec: - override protected def jsonStringDeserializer: JsonStringDeserializer = { + override protected def jsonStringDeserializer: JsonStringDeserializer = SprayJsonStringDeserializer - } "SprayJsonStringDeserializer" - { verifyValidationWithHmacAlgorithms() } -} diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala index 9d656ff..96d3ac4 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala @@ -3,14 +3,12 @@ package org.janjaali.sprayjwt.sprayjson import org.janjaali.sprayjwt.algorithms.{Algorithm, JsonStringSerializerSpec} import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} -final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec { +final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec: - override final protected def jsonStringSerializer: JsonStringSerializer = { + override final protected def jsonStringSerializer: JsonStringSerializer = SprayJsonStringSerializer - } "SprayJsonStringSerializer" - { verifySignWithHmacAlgorithms() } -} From 6062dc65af7fc3f8babe1653e081409a75caad59 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 11 May 2021 20:20:42 +0200 Subject: [PATCH 38/43] WIP. --- build.sbt | 6 ++-- .../SprayJsonStringDeserializerSpec.scala | 1 - .../org/janjaali/sprayjwt/JwtClaims.scala | 13 -------- .../sprayjwt/algorithms/Algorithm.scala | 12 +++---- .../janjaali/sprayjwt/headers/headers.scala | 2 +- .../json/JsonStringDeserializer.scala | 2 +- .../sprayjwt/json/JsonStringSerializer.scala | 2 +- .../janjaali/sprayjwt/json/JsonValue.scala | 2 +- .../JsonStringDeserializerSpec.scala | 31 ++++++------------- .../sprayjwt/jwt/ScalaCheckGenerators.scala | 2 +- .../sprayjwt/tests/ScalaTestSpec.scala | 4 +-- 11 files changed, 25 insertions(+), 52 deletions(-) delete mode 100644 spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala diff --git a/build.sbt b/build.sbt index e58244b..55754ef 100644 --- a/build.sbt +++ b/build.sbt @@ -43,10 +43,10 @@ lazy val sprayJwt = (project in file("spray-jwt")) // Encryption "org.bouncycastle" % "bcpkix-jdk15on" % "1.58", // Test - ("org.scalatest" %% "scalatest" % "3.2.7" % Test).cross(CrossVersion.for3Use2_13), + "org.scalatest" %% "scalatest" % "3.2.8" % Test, // Property based tests - ("org.scalacheck" %% "scalacheck" % "1.15.3" % Test).cross(CrossVersion.for3Use2_13), - ("org.scalatestplus" %% "scalacheck-1-15" % "3.2.6.0" % Test).cross(CrossVersion.for3Use2_13) + "org.scalacheck" %% "scalacheck" % "1.15.3" % Test, + "org.scalatestplus" %% "scalacheck-1-15" % "3.2.8.0" % Test ) ) diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala index f07a00e..320b0b9 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala @@ -9,6 +9,5 @@ final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec: SprayJsonStringDeserializer "SprayJsonStringDeserializer" - { - verifyValidationWithHmacAlgorithms() } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala deleted file mode 100644 index d9440e1..0000000 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/JwtClaims.scala +++ /dev/null @@ -1,13 +0,0 @@ -package org.janjaali.sprayjwt - -@deprecated("Use jwt.model.Claim.scala") -case class JwtClaims( - iss: Option[String] = None, - sub: Option[String] = None, - aud: Option[Set[String]] = None, - exp: Option[Long] = None, - nbf: Option[Long] = None, - isa: Option[Long] = None, - iat: Option[Long] = None, - jti: Option[String] = None -) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 83aae84..01f315d 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -130,19 +130,19 @@ object Algorithm { /** HMAC using SHA-256 */ - final case object Hs256 extends Hmac { + case object Hs256 extends Hmac { override val hashingAlgorithmName = "HMACSHA256" } /** HMAC using SHA-384 */ - final case object Hs384 extends Hmac { + case object Hs384 extends Hmac { override val hashingAlgorithmName = "HMACSHA384" } /** HMAC using SHA-512 */ - final case object Hs512 extends Hmac { + case object Hs512 extends Hmac { override val hashingAlgorithmName = "HMACSHA512" } } @@ -248,19 +248,19 @@ object Algorithm { /** RSASSA-PKCS1-v1_5 using SHA-256 */ - final case object Rs256 extends Rsa { + case object Rs256 extends Rsa { override protected def hashingAlgorithmName: String = "SHA256withRSA" } /** RSASSA-PKCS1-v1_5 using SHA-384 */ - final case object Rs384 extends Rsa { + case object Rs384 extends Rsa { override protected def hashingAlgorithmName: String = "SHA384withRSA" } /** RSASSA-PKCS1-v1_5 using SHA-512 */ - final case object Rs512 extends Rsa { + case object Rs512 extends Rsa { override protected def hashingAlgorithmName: String = "SHA512withRSA" } } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala index 451228a..9ad517f 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala @@ -47,7 +47,7 @@ package object headers { json.asJsObject.getFields("alg", "typ") match { case Seq(JsString(alg), JsString(typ)) if typ == "JWT" => - val maybeAlgorithm = alg match { + val maybeAlgorithm: Option[Algorithm] = alg match { case "HS256" => Some(Algorithm.Hmac.Hs256) case "HS384" => Some(Algorithm.Hmac.Hs384) case "HS512" => Some(Algorithm.Hmac.Hs512) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala index 6dab137..b92b160 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala @@ -4,7 +4,7 @@ package org.janjaali.sprayjwt.json trait JsonStringDeserializer { - final object Implicits { + object Implicits { implicit val implicitDeserialize: String => JsonValue = deserialize } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala index a1def4c..6ee9342 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -4,7 +4,7 @@ package org.janjaali.sprayjwt.json trait JsonStringSerializer { - final object Implicits { + object Implicits { implicit val implicitSerialize: JsonValue => String = serialize } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 398a406..89a3405 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -13,4 +13,4 @@ final case class JsonNumber(value: BigDecimal) extends JsonValue final case class JsonBoolean(value: Boolean) extends JsonValue -final case object JsonNull extends JsonValue +case object JsonNull extends JsonValue diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala index eea7041..c39fce2 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala @@ -4,7 +4,7 @@ import org.janjaali.sprayjwt.encoder.{Base64UrlDecoder, Base64UrlEncoder} import org.janjaali.sprayjwt.json.{JsonStringDeserializer, JsonValue} import org.janjaali.sprayjwt.tests.ScalaTestSpec -trait JsonStringDeserializerSpec extends ScalaTestSpec { +trait JsonStringDeserializerSpec extends ScalaTestSpec: private implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder @@ -12,17 +12,13 @@ trait JsonStringDeserializerSpec extends ScalaTestSpec { protected def jsonStringDeserializer: JsonStringDeserializer - protected def verifyValidationWithHmacAlgorithms(): Unit = { - + protected def verifyValidationWithHmacAlgorithms(): Unit = verifyValidationWithHmac256Algorithm() - verifyValidationWithHmac384Algorithm() - verifyValidationWithHmac512Algorithm() - } - - private def verifyValidationWithHmac256Algorithm(): Unit = { + + private def verifyValidationWithHmac256Algorithm(): Unit = verifyValidationWithHmacAlgorithm( algorithmName = "Hmac256", data = { @@ -30,10 +26,8 @@ trait JsonStringDeserializerSpec extends ScalaTestSpec { }, secret = Secret("secret value") ) - } - - private def verifyValidationWithHmac384Algorithm(): Unit = { + private def verifyValidationWithHmac384Algorithm(): Unit = verifyValidationWithHmacAlgorithm( algorithmName = "Hmac384", data = { @@ -41,10 +35,8 @@ trait JsonStringDeserializerSpec extends ScalaTestSpec { }, secret = Secret("secret value") ) - } - - private def verifyValidationWithHmac512Algorithm(): Unit = { + private def verifyValidationWithHmac512Algorithm(): Unit = verifyValidationWithHmacAlgorithm( algorithmName = "Hmac512", data = { @@ -52,21 +44,16 @@ trait JsonStringDeserializerSpec extends ScalaTestSpec { }, secret = Secret("secret value") ) - } private def verifyValidationWithHmacAlgorithm( algorithmName: String, data: String, secret: Secret - ): Unit = { - + ): Unit = s"Verify deserializer with '$algorithmName'." in { - Algorithm.validate(data, secret) shouldBe true } - } - private implicit def serializeJson: String => JsonValue = { + private implicit def serializeJson: String => JsonValue = jsonStringDeserializer.deserialize - } -} + \ No newline at end of file diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala index ac0a255..d25e433 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jwt/ScalaCheckGenerators.scala @@ -34,7 +34,7 @@ object ScalaCheckGenerators { * @return generator for expiration time */ def expirationTimeClaimGen: Gen[Claim.ExpirationTime] = { - numericDateGen.map(Claim.ExpirationTime) + numericDateGen.map(Claim.ExpirationTime.apply) } def privateClaimGen: Gen[Claim.Private[_]] = { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala index 6f19f47..16d8dc1 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/tests/ScalaTestSpec.scala @@ -1,6 +1,6 @@ package org.janjaali.sprayjwt.tests -import org.scalatest.matchers.should.Matchers +import org.scalatest.matchers.should import org.scalatest.freespec.AnyFreeSpec -trait ScalaTestSpec extends AnyFreeSpec with Matchers +trait ScalaTestSpec extends AnyFreeSpec with should.Matchers From 24b70265d4674079c54a230240d219bc0543b4d5 Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 11 May 2021 21:58:42 +0200 Subject: [PATCH 39/43] WIP. --- .scalafmt.conf | 4 ++- .../SprayJsonStringDeserializer.scala | 19 +++++------- .../sprayjson/SprayJsonStringSerializer.scala | 30 +++++++++++-------- .../SprayJsonStringDeserializerSpec.scala | 3 +- .../SprayJsonStringSerializerSpec.scala | 4 +-- .../json/JsonStringDeserializer.scala | 20 ++++++++----- .../sprayjwt/json/JsonStringSerializer.scala | 19 +++++++----- .../janjaali/sprayjwt/json/JsonValue.scala | 30 +++++++++++++++++-- 8 files changed, 82 insertions(+), 47 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index e46ff79..4d9c342 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,8 @@ version = "3.0.0-RC2" +runner.dialect = scala3 + newlines.topLevelStatements = [before] newlines.afterCurlyLambdaParams = preserve -runner.dialect = scala3 +docstrings.wrap = "no" diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala index 5a1749e..c42dd31 100644 --- a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializer.scala @@ -1,8 +1,10 @@ package org.janjaali.sprayjwt.sprayjson -import org.janjaali.sprayjwt.json._ -import spray.json._ +import org.janjaali.sprayjwt.json.* +import spray.json.* +/** spray-json implementation of the JsonStringDeserializer. + */ object SprayJsonStringDeserializer extends JsonStringDeserializer: override def deserialize(jsonText: String): JsonValue = @@ -18,14 +20,9 @@ object SprayJsonStringDeserializer extends JsonStringDeserializer: ) case JsArray(elements) => - ??? - - case JsString(value) => - JsonString(value) - - case JsNumber(value) => - JsonNumber(value) + JsonArray(elements.map(asJsonValue)) + case JsString(value) => JsonString(value) + case JsNumber(value) => JsonNumber(value) case jsBoolean: JsBoolean => JsonBoolean(jsBoolean.value) - - case JsNull => JsonNull + case JsNull => JsonNull diff --git a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala index 7311334..f229227 100644 --- a/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala +++ b/spray-json-support/src/main/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializer.scala @@ -1,26 +1,30 @@ package org.janjaali.sprayjwt.sprayjson -import org.janjaali.sprayjwt.json._ -import spray.json._ +import org.janjaali.sprayjwt.json.* +import spray.json.* +/** spray-json implementation of the JsonStringSerializer. + */ object SprayJsonStringSerializer extends JsonStringSerializer: override def serialize(jsonValue: JsonValue): String = - sprayJsonValue(jsonValue).compactPrint + asJsValue(jsonValue).compactPrint - private def sprayJsonValue(jsonValue: JsonValue): JsValue = + private def asJsValue(jsonValue: JsonValue): JsValue = jsonValue match case JsonObject(members) => JsObject( members.map { case (key, value) => - key -> sprayJsonValue(value) + key -> asJsValue(value) }.toMap ) - case JsonString(value) => - JsString(value) - case JsonNumber(value) => - JsNumber(value) - case JsonBoolean(value) => - JsBoolean(value) - case JsonNull => - JsNull + + case JsonArray(elements) => + JsArray( + elements.map(asJsValue).toVector + ) + + case JsonString(value) => JsString(value) + case JsonNumber(value) => JsNumber(value) + case JsonBoolean(value) => JsBoolean(value) + case JsonNull => JsNull diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala index 320b0b9..12dfbcf 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala @@ -5,9 +5,10 @@ import org.janjaali.sprayjwt.json.JsonStringDeserializer final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec: - override protected def jsonStringDeserializer: JsonStringDeserializer = + override final protected def jsonStringDeserializer: JsonStringDeserializer = SprayJsonStringDeserializer "SprayJsonStringDeserializer" - { + verifyValidationWithHmacAlgorithms() } diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala index 96d3ac4..82c8ed9 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala @@ -1,7 +1,7 @@ package org.janjaali.sprayjwt.sprayjson -import org.janjaali.sprayjwt.algorithms.{Algorithm, JsonStringSerializerSpec} -import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} +import org.janjaali.sprayjwt.algorithms.JsonStringSerializerSpec +import org.janjaali.sprayjwt.json.JsonStringSerializer final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec: diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala index b92b160..ab6bb4b 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala @@ -1,13 +1,17 @@ package org.janjaali.sprayjwt.json -// TODO: Docs. +/** Provides a JSON string deserializer. + */ +trait JsonStringDeserializer: -trait JsonStringDeserializer { - - object Implicits { - - implicit val implicitDeserialize: String => JsonValue = deserialize - } + /** Gives a JSON string deserializer. + */ + given stringDeserializer: (String => JsonValue) with + def apply(jsonText: String): JsonValue = deserialize(jsonText) + /** Deserializes a JSON string as JsonValue. + * + * @param jsonText JSON string that should be deserialized + * @return json value + */ def deserialize(jsonText: String): JsonValue -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala index 6ee9342..a2f0b71 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -1,13 +1,16 @@ package org.janjaali.sprayjwt.json -// TODO: Docs. +/** Provides a JSON string serializer. + */ +trait JsonStringSerializer: -trait JsonStringSerializer { - - object Implicits { - - implicit val implicitSerialize: JsonValue => String = serialize - } + /** Gives a JSON string serializer. + */ + given stringSerializer: (JsonValue => String) with + def apply(json: JsonValue): String = serialize(json) + /** Serializes a JSON value as a string. + * + * @param json JSON value that should be serialized + */ def serialize(json: JsonValue): String -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 89a3405..4086f5b 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -1,16 +1,40 @@ package org.janjaali.sprayjwt.json -// TODO: Check accessibility. -// TODO: Add docs. - +/** Represents a JSON value. + */ sealed trait JsonValue +/** Represents a JSON object consisting of a set of members (i.e. name -> JSON + * value pairs). + * + * @param members set of name -> JSON value pairs + */ final case class JsonObject(members: Map[String, JsonValue]) extends JsonValue +/** Represents a JSON array consisting of a set of elements (i.e. JSON values). + * + * @param elements set of JSON values + */ +final case class JsonArray(elements: Seq[JsonValue]) extends JsonValue + +/** Represents a JSON string. + * + * @param value string value + */ final case class JsonString(value: String) extends JsonValue +/** Represents a JSON number. + * + * @param value number value + */ final case class JsonNumber(value: BigDecimal) extends JsonValue +/** Represents a JSON boolean. + * + * @param value boolean value + */ final case class JsonBoolean(value: Boolean) extends JsonValue +/** Represents a JSON null value. + */ case object JsonNull extends JsonValue From 215087fa277d95ed3623b21c9a23c9fb0c596c5d Mon Sep 17 00:00:00 2001 From: Siyavash Habashi Date: Tue, 11 May 2021 23:12:50 +0200 Subject: [PATCH 40/43] WIP --- .../sprayjwt/algorithms/Algorithm.scala | 126 ++++++------------ .../sprayjwt/encoder/Base64UrlEncoder.scala | 9 +- .../json/JsonStringDeserializer.scala | 2 +- .../sprayjwt/json/JsonStringSerializer.scala | 2 +- .../org/janjaali/sprayjwt/jws/Header.scala | 13 +- .../algorithms/JsonStringSerializerSpec.scala | 34 ++--- 6 files changed, 59 insertions(+), 127 deletions(-) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 01f315d..6965f9c 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -10,15 +10,17 @@ import org.janjaali.sprayjwt.encoder.{ Base64UrlEncoder, ByteEncoder } -import org.janjaali.sprayjwt.json._ +import org.janjaali.sprayjwt.json.* import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} import java.io.{IOException, StringReader} import java.security.{PrivateKey, PublicKey, Signature} /** Represents a cryptographic algorithm used with JWT. + * + * @param base64UrlEncoder Base64 encoder that is used */ -sealed trait Algorithm { +sealed trait Algorithm(protected val base64UrlEncoder: Base64UrlEncoder): /** Digitally signs the protected headers of the given Jose Header and the Jws * Payload. @@ -31,26 +33,22 @@ sealed trait Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret - )(implicit - serializeJson: JsonValue => String, - base64encoder: Base64UrlEncoder - ): JwsSignature + )(using jsonStringSerializer: JsonStringSerializer): JwsSignature // TODO: Add docs. def validate( data: String, secret: Secret - )(implicit base64UrlEncoder: Base64UrlEncoder): Boolean -} + ): Boolean -/** Provides algorithms. +/** Algorithms. */ -object Algorithm { +object Algorithm: /** Hash-based Message Authentication Codes (HMACs) algorithm to sign and * validate digital signatures. */ - sealed trait Hmac extends Algorithm { + sealed trait Hmac extends Algorithm: protected def hashingAlgorithmName: String @@ -58,38 +56,26 @@ object Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret - )(implicit - serializeJson: JsonValue => String, - base64UrlEncoder: Base64UrlEncoder - ): JwsSignature = { - - val base64UrlEncodedJoseHeader = { - base64UrlEncoder.encode { - serializeJson { - joseHeader.asJson - } - } - } - - val base64UrlEncodedJwsPayload = { - base64UrlEncoder.encode { - serializeJson { - jwsPayload.asJson - } - } - } - - val inputToBeSigned = { + )(using jsonStringSerializer: JsonStringSerializer): JwsSignature = + val base64UrlEncodedJoseHeader = + base64UrlEncoder.encode( + jsonStringSerializer.serialize(joseHeader.asJson) + ) + + val base64UrlEncodedJwsPayload = + base64UrlEncoder.encode( + jsonStringSerializer.serialize(jwsPayload.asJson) + ) + + val inputToBeSigned = s"$base64UrlEncodedJoseHeader.$base64UrlEncodedJwsPayload" - } sign(inputToBeSigned, secret) - } override def validate( data: String, secret: Secret - )(implicit base64UrlEncoder: Base64UrlEncoder): Boolean = { + ): Boolean = { data.split("\\.") match { case Array( @@ -108,11 +94,7 @@ object Algorithm { } } - private def sign( - data: String, - secret: Secret - )(implicit base64UrlEncoder: Base64UrlEncoder): JwsSignature = { - + private def sign(data: String, secret: Secret): JwsSignature = val mac = Mac.getInstance(hashingAlgorithmName) val key = new SecretKeySpec(secret.asByteArray, hashingAlgorithmName) @@ -121,36 +103,30 @@ object Algorithm { val signature = mac.doFinal(data.getBytes("UTF-8")) JwsSignature(base64UrlEncoder.encode(signature)) - } - } /** Provides HMAC algorithms. */ - object Hmac { + object Hmac: - /** HMAC using SHA-256 + /** HMAC using SHA-256. */ - case object Hs256 extends Hmac { + case object Hs256 extends Algorithm(Base64UrlEncoder), Hmac: override val hashingAlgorithmName = "HMACSHA256" - } - /** HMAC using SHA-384 + /** HMAC using SHA-384. */ - case object Hs384 extends Hmac { + case object Hs384 extends Algorithm(Base64UrlEncoder), Hmac: override val hashingAlgorithmName = "HMACSHA384" - } - /** HMAC using SHA-512 + /** HMAC using SHA-512. */ - case object Hs512 extends Hmac { + case object Hs512 extends Algorithm(Base64UrlEncoder), Hmac: override val hashingAlgorithmName = "HMACSHA512" - } - } /** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign * and validate digital signatures. */ - sealed trait Rsa extends Algorithm { + sealed trait Rsa extends Algorithm: private val provider = "BC" @@ -160,22 +136,13 @@ object Algorithm { joseHeader: JoseHeader, jwsPayload: JwsPayload, secret: Secret - )(implicit - serializeJson: JsonValue => String, - base64UrlEncoder: Base64UrlEncoder - ): JwsSignature = { - - ??? - } + )(using jsonStringSerializer: JsonStringSerializer): JwsSignature = ??? // TODO: Check implementation def sign( data: String, secret: String - )(implicit - serializeJson: JsonValue => String, - base64UrlEncoder: Base64UrlEncoder - ): String = { + )(using serializeJson: JsonValue => String): String = { val key = getPrivateKey(secret) @@ -208,12 +175,7 @@ object Algorithm { rsaSignature.verify(Base64UrlDecoder.decode(signature)) } - override def validate( - data: String, - secret: Secret - )(implicit base64encoder: Base64UrlEncoder): Boolean = { - ??? - } + override def validate(data: String, secret: Secret): Boolean = ??? private def getPublicKey(str: String): PublicKey = { val pemParser = new PEMParser(new StringReader(str)) @@ -240,30 +202,25 @@ object Algorithm { throw new IOException(s"Invalid key for $hashingAlgorithmName") } } - } /** Provides RSA algorithms. */ - object Rsa { + object Rsa: // TODO: Rename - /** RSASSA-PKCS1-v1_5 using SHA-256 + /** RSASSA-PKCS1-v1_5 using SHA-256. */ - case object Rs256 extends Rsa { + case object Rs256 extends Algorithm(Base64UrlEncoder), Rsa: override protected def hashingAlgorithmName: String = "SHA256withRSA" - } - /** RSASSA-PKCS1-v1_5 using SHA-384 + /** RSASSA-PKCS1-v1_5 using SHA-384. */ - case object Rs384 extends Rsa { + case object Rs384 extends Algorithm(Base64UrlEncoder), Rsa: override protected def hashingAlgorithmName: String = "SHA384withRSA" - } - /** RSASSA-PKCS1-v1_5 using SHA-512 + /** RSASSA-PKCS1-v1_5 using SHA-512. */ - case object Rs512 extends Rsa { + case object Rs512 extends Algorithm(Base64UrlEncoder), Rsa: override protected def hashingAlgorithmName: String = "SHA512withRSA" - } - } // TODO: Docs. // TODO: Test. @@ -313,4 +270,3 @@ object Algorithm { false } } -} diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala index 22da6e6..06a9d51 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/encoder/Base64UrlEncoder.scala @@ -5,7 +5,7 @@ import java.util.Base64 /** Default Base64 URL encoder that uses internally the [[java.util.Base64]] URL * encoder without padding. */ -private[sprayjwt] trait Base64UrlEncoder { +private[sprayjwt] trait Base64UrlEncoder: private lazy val encoder: Base64.Encoder = Base64.getUrlEncoder.withoutPadding @@ -14,19 +14,16 @@ private[sprayjwt] trait Base64UrlEncoder { * @param text text that should be encoded * @return Base64 URL encoded String */ - def encode(text: String): String = { + def encode(text: String): String = encode(text.getBytes("UTF-8")) - } /** Encodes a byte-array as Base64 URL encoded String. * * @param byteArray ByteArray to encode as String * @return String encoded ByteArray */ - def encode(byteArray: Array[Byte]): String = { + def encode(byteArray: Array[Byte]): String = encoder.encodeToString(byteArray) - } -} /** Base64 URL encoder utilities. */ diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala index ab6bb4b..bd55082 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringDeserializer.scala @@ -6,7 +6,7 @@ trait JsonStringDeserializer: /** Gives a JSON string deserializer. */ - given stringDeserializer: (String => JsonValue) with + given (String => JsonValue) with def apply(jsonText: String): JsonValue = deserialize(jsonText) /** Deserializes a JSON string as JsonValue. diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala index a2f0b71..0382adb 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonStringSerializer.scala @@ -6,7 +6,7 @@ trait JsonStringSerializer: /** Gives a JSON string serializer. */ - given stringSerializer: (JsonValue => String) with + given (JsonValue => String) with def apply(json: JsonValue): String = serialize(json) /** Serializes a JSON value as a string. diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala index c636485..7df90b9 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala @@ -63,21 +63,20 @@ object Header { private def valueJsonWriter: JsonWriter[algorithms.Algorithm] = { new JsonWriter[algorithms.Algorithm] { - override def write(algorithm: algorithms.Algorithm): JsonValue = { - algorithm match { + override def write(algorithm: algorithms.Algorithm): JsonValue = + algorithm match case algorithms.Algorithm.Rsa.Rs256 => JsonString("RS256") case algorithms.Algorithm.Rsa.Rs384 => JsonString("RS384") case algorithms.Algorithm.Rsa.Rs512 => JsonString("RS512") case algorithms.Algorithm.Hmac.Hs256 => JsonString("HS256") case algorithms.Algorithm.Hmac.Hs384 => JsonString("HS384") case algorithms.Algorithm.Hmac.Hs512 => JsonString("HS512") - } - } + } } - private[Header] def apply(algorithmName: String): Option[Algorithm] = { - algorithmName match { + private[Header] def apply(algorithmName: String): Option[Algorithm] = + algorithmName match case "RS256" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs256)) case "RS384" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs384)) case "RS512" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs512)) @@ -85,8 +84,6 @@ object Header { case "HS384" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs384)) case "HS512" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs512)) case _ => None - } - } } /** Declares the media type of the complete JWS. diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala index 7ad8f64..3b578ef 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala @@ -7,58 +7,46 @@ import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} import org.janjaali.sprayjwt.tests.ScalaTestSpec -trait JsonStringSerializerSpec extends ScalaTestSpec { +trait JsonStringSerializerSpec extends ScalaTestSpec: - private implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder + private given Base64UrlEncoder = Base64UrlEncoder - protected def jsonStringSerializer: JsonStringSerializer + protected given jsonStringSerializer: JsonStringSerializer - protected def verifySignWithHmacAlgorithms(): Unit = { - + protected def verifySignWithHmacAlgorithms(): Unit = verifySignWithHmac256Algorithm() - verifySignWithHmac384Algorithm() - verifySignWithHmac512Algorithm() - } - - private def verifySignWithHmac256Algorithm(): Unit = { + private def verifySignWithHmac256Algorithm(): Unit = verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs256, expectedSignature = JwsSignature( "jUzTJEnlFeTXDUPp9vJMwoalvXJ55IZ6DaBExN08UtA" ) ) - } - - private def verifySignWithHmac384Algorithm(): Unit = { + private def verifySignWithHmac384Algorithm(): Unit = verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs384, expectedSignature = JwsSignature( "tz6NV8IfhPNqEnfUgeu0TJowwvWsjcmFCiRC_F-7bTOQeUle8jomj151nYHx1-IQ" ) ) - } - - private def verifySignWithHmac512Algorithm(): Unit = { + private def verifySignWithHmac512Algorithm(): Unit = verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs512, expectedSignature = JwsSignature( "dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w" ) ) - } private def verifySignWithAlgorithm( algorithm: Algorithm, expectedSignature: JwsSignature - ): Unit = { - + ): Unit = s"Verify serializer with algorithm '${algorithm}'." in { - val joseHeader = JoseHeader( Seq( Header.Type(Header.Type.Value.Jwt), @@ -83,9 +71,3 @@ trait JsonStringSerializerSpec extends ScalaTestSpec { algorithm.sign(joseHeader, jwsPayload, secret) shouldBe expectedSignature } - } - - private implicit def serializeJson: JsonValue => String = { - jsonStringSerializer.serialize - } -} From 719b5a91b0889af21f9036d0aa66d4eb6b955a1e Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sun, 16 May 2021 21:59:42 +0200 Subject: [PATCH 41/43] WIP. --- .../SprayJsonStringDeserializerSpec.scala | 90 +++++++++++++++++-- .../SprayJsonStringSerializerSpec.scala | 88 ++++++++++++++++-- .../sprayjson/SprayJsonSupportSpec.scala | 20 +++++ .../sprayjwt/algorithms/Algorithm.scala | 6 +- .../janjaali/sprayjwt/json/JsonValue.scala | 14 +++ .../JsonStringDeserializerSpec.scala | 59 ------------ ...alizerSpec.scala => JsonSupportSpec.scala} | 55 +++++++++++- 7 files changed, 253 insertions(+), 79 deletions(-) create mode 100644 spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonSupportSpec.scala delete mode 100644 spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala rename spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/{JsonStringSerializerSpec.scala => JsonSupportSpec.scala} (53%) diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala index 12dfbcf..d324757 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringDeserializerSpec.scala @@ -1,14 +1,90 @@ package org.janjaali.sprayjwt.sprayjson -import org.janjaali.sprayjwt.algorithms.JsonStringDeserializerSpec -import org.janjaali.sprayjwt.json.JsonStringDeserializer +import org.janjaali.sprayjwt.json._ +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import spray.json._ -final class SprayJsonStringDeserializerSpec extends JsonStringDeserializerSpec: - - override final protected def jsonStringDeserializer: JsonStringDeserializer = - SprayJsonStringDeserializer +final class SprayJsonStringDeserializerSpec extends ScalaTestSpec: "SprayJsonStringDeserializer" - { - verifyValidationWithHmacAlgorithms() + val sut = SprayJsonStringDeserializer + + "deserializes" - { + + "JSON objects" - { + + "that are empty." in { + + val jsonText = "{}" + + sut.deserialize(jsonText) shouldBe JsonObject.empty + } + + "that are not empty." in { + + val jsonText = """{ + | "key":"value", + | "otherKey":42} + |""".stripMargin + + sut.deserialize(jsonText) shouldBe JsonObject( + Map( + "key" -> JsonString("value"), + "otherKey" -> JsonNumber(42) + ) + ) + } + } + + "JSON arrays" - { + + "that are empty." in { + + val jsonText = "[]" + + sut.deserialize(jsonText) shouldBe JsonArray.empty + } + + "that are not empty." in { + + val jsonText = """["value",42]""" + + sut.deserialize(jsonText) shouldBe JsonArray( + Seq( + JsonString("value"), + JsonNumber(42) + ) + ) + } + } + + "JSON strings." in { + + val jsonText = "\"dance\"" + + sut.deserialize(jsonText) shouldBe JsonString("dance") + } + + "JSON numbers." in { + + val jsonText = "42" + + sut.deserialize(jsonText) shouldBe JsonNumber(42) + } + + "JSON booleans." in { + + val jsonText = "true" + + sut.deserialize(jsonText) shouldBe JsonBoolean(true) + } + + "JSON nulls." in { + + val jsonText = "null" + + sut.deserialize(jsonText) shouldBe JsonNull + } + } } diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala index 82c8ed9..a9e7ddb 100644 --- a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonStringSerializerSpec.scala @@ -1,14 +1,88 @@ package org.janjaali.sprayjwt.sprayjson -import org.janjaali.sprayjwt.algorithms.JsonStringSerializerSpec -import org.janjaali.sprayjwt.json.JsonStringSerializer +import org.janjaali.sprayjwt.json._ +import org.janjaali.sprayjwt.tests.ScalaTestSpec +import spray.json._ -final class SprayJsonStringSerializerSpec extends JsonStringSerializerSpec: - - override final protected def jsonStringSerializer: JsonStringSerializer = - SprayJsonStringSerializer +final class SprayJsonStringSerializerSpec extends ScalaTestSpec: "SprayJsonStringSerializer" - { - verifySignWithHmacAlgorithms() + val sut = SprayJsonStringSerializer + + "serializes" - { + + "JSON objects" - { + + "that are empty." in { + + val jsonValue = JsonObject.empty + + sut.serialize(jsonValue) shouldBe "{}" + } + + "that are not empty." in { + + val jsonValue = JsonObject( + Map( + "key" -> JsonString("value"), + "otherKey" -> JsonNumber(42) + ) + ) + + sut.serialize(jsonValue) shouldBe + """{"key":"value","otherKey":42}""" + } + } + + "JSON arrays" - { + + "that are empty." in { + + val jsonValue = JsonArray.empty + + sut.serialize(jsonValue) shouldBe "[]" + } + + "that are not empty." in { + + val jsonValue = JsonArray( + Seq( + JsonString("value"), + JsonNumber(42) + ) + ) + + sut.serialize(jsonValue) shouldBe """["value",42]""" + } + } + + "JSON strings." in { + + val jsonValue = JsonString("dance") + + sut.serialize(jsonValue) shouldBe "\"dance\"" + } + + "JSON numbers." in { + + val jsonValue = JsonNumber(42) + + sut.serialize(jsonValue) shouldBe "42" + } + + "JSON booleans." in { + + val jsonValue = JsonBoolean(true) + + sut.serialize(jsonValue) shouldBe "true" + } + + "JSON nulls." in { + + val jsonValue = JsonNull + + sut.serialize(jsonValue) shouldBe "null" + } + } } diff --git a/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonSupportSpec.scala b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonSupportSpec.scala new file mode 100644 index 0000000..5b52692 --- /dev/null +++ b/spray-json-support/src/test/scala/org/janjaali/sprayjwt/sprayjson/SprayJsonSupportSpec.scala @@ -0,0 +1,20 @@ +package org.janjaali.sprayjwt.sprayjson + +import org.janjaali.sprayjwt.algorithms.JsonSupportSpec +import org.janjaali.sprayjwt.json.JsonStringSerializer +import org.janjaali.sprayjwt.json.JsonStringDeserializer + +final class SprayJsonSupportSpec extends JsonSupportSpec: + + override protected given jsonStringSerializer: JsonStringSerializer = + SprayJsonStringSerializer + + override protected given jsonStringDeserializer: JsonStringDeserializer = + SprayJsonStringDeserializer + + "spray-json support" - { + + verifySignWithHmacAlgorithms() + + verifyValidationWithHmacAlgorithms() + } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 6965f9c..88a8035 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -227,8 +227,8 @@ object Algorithm: def validate( data: String, secret: Secret // TODO: Do RSA algorithms use the same kind of secret, probably not? - )(implicit - deserializeJson: String => JsonValue, + )(using + jsonStringDeserializer: JsonStringDeserializer, base64UrlDecoder: Base64UrlDecoder, base64UrlEncoder: Base64UrlEncoder ): Boolean = { @@ -240,7 +240,7 @@ object Algorithm: base64UrlEncodedJwsPayload, base64EncodedSignature ) => - val maybeJoseHeaderJsonObject = deserializeJson { + val maybeJoseHeaderJsonObject = jsonStringDeserializer.deserialize { base64UrlDecoder.decodeAsString { base64UrlEncodedJoseHeader } diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala index 4086f5b..6bf2f80 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/json/JsonValue.scala @@ -11,12 +11,26 @@ sealed trait JsonValue */ final case class JsonObject(members: Map[String, JsonValue]) extends JsonValue +/** JSON object auxillary constructors and methods. + */ +object JsonObject: + + /** Constructs an empty JSON object. */ + lazy val empty: JsonObject = JsonObject(Map.empty) + /** Represents a JSON array consisting of a set of elements (i.e. JSON values). * * @param elements set of JSON values */ final case class JsonArray(elements: Seq[JsonValue]) extends JsonValue +/** JSON array auxillary constructors and methods. + */ +object JsonArray: + + /** Constructs an empty JSON object. */ + lazy val empty: JsonArray = JsonArray(Seq.empty) + /** Represents a JSON string. * * @param value string value diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala deleted file mode 100644 index c39fce2..0000000 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringDeserializerSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.janjaali.sprayjwt.algorithms - -import org.janjaali.sprayjwt.encoder.{Base64UrlDecoder, Base64UrlEncoder} -import org.janjaali.sprayjwt.json.{JsonStringDeserializer, JsonValue} -import org.janjaali.sprayjwt.tests.ScalaTestSpec - -trait JsonStringDeserializerSpec extends ScalaTestSpec: - - private implicit val base64UrlEncoder: Base64UrlEncoder = Base64UrlEncoder - - private implicit val base64UrlDecoder: Base64UrlDecoder = Base64UrlDecoder - - protected def jsonStringDeserializer: JsonStringDeserializer - - protected def verifyValidationWithHmacAlgorithms(): Unit = - verifyValidationWithHmac256Algorithm() - verifyValidationWithHmac384Algorithm() - verifyValidationWithHmac512Algorithm() - - - private def verifyValidationWithHmac256Algorithm(): Unit = - verifyValidationWithHmacAlgorithm( - algorithmName = "Hmac256", - data = { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.eMS0OOcs-0Eo5x5vDepYOcITeG4NtPrtE8seTsT1RT0" - }, - secret = Secret("secret value") - ) - - private def verifyValidationWithHmac384Algorithm(): Unit = - verifyValidationWithHmacAlgorithm( - algorithmName = "Hmac384", - data = { - "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tXvOFupOdp6ISki7ysqn_gO9LHk2n35d0f_E26d9FYfhLLHNudoWU2HXD_6Tnm7X" - }, - secret = Secret("secret value") - ) - - private def verifyValidationWithHmac512Algorithm(): Unit = - verifyValidationWithHmacAlgorithm( - algorithmName = "Hmac512", - data = { - "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.mV13yTaWA-6cQWcQEmUOh2qaTnpF5hQpNNkIMY6RIlbyQQQ-MbE9zjQ19dTQpQ2hxEql5ObbmDmWzqx13ka6Iw", - }, - secret = Secret("secret value") - ) - - private def verifyValidationWithHmacAlgorithm( - algorithmName: String, - data: String, - secret: Secret - ): Unit = - s"Verify deserializer with '$algorithmName'." in { - Algorithm.validate(data, secret) shouldBe true - } - - private implicit def serializeJson: String => JsonValue = - jsonStringDeserializer.deserialize - \ No newline at end of file diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala similarity index 53% rename from spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala rename to spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala index 3b578ef..c106a6c 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonStringSerializerSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala @@ -1,23 +1,36 @@ package org.janjaali.sprayjwt.algorithms -import org.janjaali.sprayjwt.encoder.Base64UrlEncoder +import org.janjaali.sprayjwt.encoder.{Base64UrlDecoder, Base64UrlEncoder} import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits._ -import org.janjaali.sprayjwt.json.{JsonStringSerializer, JsonValue} +import org.janjaali.sprayjwt.json.{ + JsonStringDeserializer, + JsonStringSerializer, + JsonValue +} import org.janjaali.sprayjwt.jws.{Header, JoseHeader, JwsPayload, JwsSignature} import org.janjaali.sprayjwt.jwt.{Claim, JwtClaimsSet, NumericDate} import org.janjaali.sprayjwt.tests.ScalaTestSpec -trait JsonStringSerializerSpec extends ScalaTestSpec: +trait JsonSupportSpec extends ScalaTestSpec: private given Base64UrlEncoder = Base64UrlEncoder + private given Base64UrlDecoder = Base64UrlDecoder + protected given jsonStringSerializer: JsonStringSerializer + protected given jsonStringDeserializer: JsonStringDeserializer + protected def verifySignWithHmacAlgorithms(): Unit = verifySignWithHmac256Algorithm() verifySignWithHmac384Algorithm() verifySignWithHmac512Algorithm() + protected def verifyValidationWithHmacAlgorithms(): Unit = + verifyValidationWithHmac256Algorithm() + verifyValidationWithHmac384Algorithm() + verifyValidationWithHmac512Algorithm() + private def verifySignWithHmac256Algorithm(): Unit = verifySignWithAlgorithm( algorithm = Algorithm.Hmac.Hs256, @@ -71,3 +84,39 @@ trait JsonStringSerializerSpec extends ScalaTestSpec: algorithm.sign(joseHeader, jwsPayload, secret) shouldBe expectedSignature } + + private def verifyValidationWithHmac256Algorithm(): Unit = + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac256", + data = { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.eMS0OOcs-0Eo5x5vDepYOcITeG4NtPrtE8seTsT1RT0" + }, + secret = Secret("secret value") + ) + + private def verifyValidationWithHmac384Algorithm(): Unit = + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac384", + data = { + "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tXvOFupOdp6ISki7ysqn_gO9LHk2n35d0f_E26d9FYfhLLHNudoWU2HXD_6Tnm7X" + }, + secret = Secret("secret value") + ) + + private def verifyValidationWithHmac512Algorithm(): Unit = + verifyValidationWithHmacAlgorithm( + algorithmName = "Hmac512", + data = { + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.mV13yTaWA-6cQWcQEmUOh2qaTnpF5hQpNNkIMY6RIlbyQQQ-MbE9zjQ19dTQpQ2hxEql5ObbmDmWzqx13ka6Iw", + }, + secret = Secret("secret value") + ) + + private def verifyValidationWithHmacAlgorithm( + algorithmName: String, + data: String, + secret: Secret + ): Unit = + s"Verify deserializer with '$algorithmName'." in { + Algorithm.validate(data, secret) shouldBe true + } From 3ff29007c4f26404029c06ca1362b6df5bcd4900 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sun, 16 May 2021 22:10:48 +0200 Subject: [PATCH 42/43] WIP. --- .../sprayjwt/algorithms/Algorithm.scala | 35 ++++++++----------- .../sprayjwt/algorithms/JsonSupportSpec.scala | 4 --- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index 88a8035..fa37dea 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -17,10 +17,8 @@ import java.io.{IOException, StringReader} import java.security.{PrivateKey, PublicKey, Signature} /** Represents a cryptographic algorithm used with JWT. - * - * @param base64UrlEncoder Base64 encoder that is used */ -sealed trait Algorithm(protected val base64UrlEncoder: Base64UrlEncoder): +sealed trait Algorithm: /** Digitally signs the protected headers of the given Jose Header and the Jws * Payload. @@ -58,12 +56,12 @@ object Algorithm: secret: Secret )(using jsonStringSerializer: JsonStringSerializer): JwsSignature = val base64UrlEncodedJoseHeader = - base64UrlEncoder.encode( + Base64UrlEncoder.encode( jsonStringSerializer.serialize(joseHeader.asJson) ) val base64UrlEncodedJwsPayload = - base64UrlEncoder.encode( + Base64UrlEncoder.encode( jsonStringSerializer.serialize(jwsPayload.asJson) ) @@ -102,7 +100,7 @@ object Algorithm: val signature = mac.doFinal(data.getBytes("UTF-8")) - JwsSignature(base64UrlEncoder.encode(signature)) + JwsSignature(Base64UrlEncoder.encode(signature)) /** Provides HMAC algorithms. */ @@ -110,17 +108,17 @@ object Algorithm: /** HMAC using SHA-256. */ - case object Hs256 extends Algorithm(Base64UrlEncoder), Hmac: + case object Hs256 extends Hmac: override val hashingAlgorithmName = "HMACSHA256" /** HMAC using SHA-384. */ - case object Hs384 extends Algorithm(Base64UrlEncoder), Hmac: + case object Hs384 extends Hmac: override val hashingAlgorithmName = "HMACSHA384" /** HMAC using SHA-512. */ - case object Hs512 extends Algorithm(Base64UrlEncoder), Hmac: + case object Hs512 extends Hmac: override val hashingAlgorithmName = "HMACSHA512" /** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign @@ -152,7 +150,7 @@ object Algorithm: signature.initSign(key) signature.update(dataByteArray) val signatureByteArray = signature.sign - base64UrlEncoder.encode(signatureByteArray) + Base64UrlEncoder.encode(signatureByteArray) } // TODO: Check implementation @@ -209,17 +207,17 @@ object Algorithm: /** RSASSA-PKCS1-v1_5 using SHA-256. */ - case object Rs256 extends Algorithm(Base64UrlEncoder), Rsa: + case object Rs256 extends Rsa: override protected def hashingAlgorithmName: String = "SHA256withRSA" /** RSASSA-PKCS1-v1_5 using SHA-384. */ - case object Rs384 extends Algorithm(Base64UrlEncoder), Rsa: + case object Rs384 extends Rsa: override protected def hashingAlgorithmName: String = "SHA384withRSA" /** RSASSA-PKCS1-v1_5 using SHA-512. */ - case object Rs512 extends Algorithm(Base64UrlEncoder), Rsa: + case object Rs512 extends Rsa: override protected def hashingAlgorithmName: String = "SHA512withRSA" // TODO: Docs. @@ -227,21 +225,17 @@ object Algorithm: def validate( data: String, secret: Secret // TODO: Do RSA algorithms use the same kind of secret, probably not? - )(using - jsonStringDeserializer: JsonStringDeserializer, - base64UrlDecoder: Base64UrlDecoder, - base64UrlEncoder: Base64UrlEncoder - ): Boolean = { + )(using jsonStringDeserializer: JsonStringDeserializer): Boolean = { val maybeAlgorithm = { - data.split("\\.") match { + data.split("\\.") match case Array( base64UrlEncodedJoseHeader, base64UrlEncodedJwsPayload, base64EncodedSignature ) => val maybeJoseHeaderJsonObject = jsonStringDeserializer.deserialize { - base64UrlDecoder.decodeAsString { + Base64UrlDecoder.decodeAsString { base64UrlEncodedJoseHeader } } @@ -260,7 +254,6 @@ object Algorithm: case _ => // TODO: Log. None - } } maybeAlgorithm match { diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala index c106a6c..cacd7c4 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala @@ -13,10 +13,6 @@ import org.janjaali.sprayjwt.tests.ScalaTestSpec trait JsonSupportSpec extends ScalaTestSpec: - private given Base64UrlEncoder = Base64UrlEncoder - - private given Base64UrlDecoder = Base64UrlDecoder - protected given jsonStringSerializer: JsonStringSerializer protected given jsonStringDeserializer: JsonStringDeserializer From ad9ebe0418e1d4aac24eeaeaccba456eeef41805 Mon Sep 17 00:00:00 2001 From: Janjaali Date: Sun, 16 May 2021 22:29:15 +0200 Subject: [PATCH 43/43] WIP. --- .../sprayjwt/algorithms/Algorithm.scala | 85 ++++++++----------- .../janjaali/sprayjwt/headers/headers.scala | 33 +++---- .../org/janjaali/sprayjwt/jws/Header.scala | 25 +++--- .../sprayjwt/algorithms/JsonSupportSpec.scala | 8 +- .../algorithms/ScalaCheckGenerators.scala | 24 ++---- .../headers/JwtHeaderJsonProtocolSpec.scala | 4 +- .../janjaali/sprayjwt/jws/HeaderSpec.scala | 13 +-- 7 files changed, 81 insertions(+), 111 deletions(-) diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala index fa37dea..88269ba 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/algorithms/Algorithm.scala @@ -21,7 +21,7 @@ import java.security.{PrivateKey, PublicKey, Signature} sealed trait Algorithm: /** Digitally signs the protected headers of the given Jose Header and the Jws - * Payload. + * Payload with a given secret. * * @param joseHeader jose header * @param jwsPayload jws payload @@ -33,15 +33,13 @@ sealed trait Algorithm: secret: Secret )(using jsonStringSerializer: JsonStringSerializer): JwsSignature - // TODO: Add docs. - def validate( - data: String, - secret: Secret - ): Boolean + /** Validates a given JWT token. + */ + def validate(data: String, secret: Secret): Boolean /** Algorithms. */ -object Algorithm: +object Algorithms: /** Hash-based Message Authentication Codes (HMACs) algorithm to sign and * validate digital signatures. @@ -73,9 +71,8 @@ object Algorithm: override def validate( data: String, secret: Secret - ): Boolean = { - - data.split("\\.") match { + ): Boolean = + data.split("\\.") match case Array( base64UrlEncodedJoseHeader, base64UrlEncodedJwsPayload, @@ -87,10 +84,7 @@ object Algorithm: sign(signedInput, secret).value == base64EncodedSignature - case _ => - false - } - } + case _ => false private def sign(data: String, secret: Secret): JwsSignature = val mac = Mac.getInstance(hashingAlgorithmName) @@ -102,25 +96,6 @@ object Algorithm: JwsSignature(Base64UrlEncoder.encode(signature)) - /** Provides HMAC algorithms. - */ - object Hmac: - - /** HMAC using SHA-256. - */ - case object Hs256 extends Hmac: - override val hashingAlgorithmName = "HMACSHA256" - - /** HMAC using SHA-384. - */ - case object Hs384 extends Hmac: - override val hashingAlgorithmName = "HMACSHA384" - - /** HMAC using SHA-512. - */ - case object Hs512 extends Hmac: - override val hashingAlgorithmName = "HMACSHA512" - /** RSASSA-PKCS1-v1_5 (RSA) based algorithm using SHA-2 hash functions to sign * and validate digital signatures. */ @@ -136,6 +111,8 @@ object Algorithm: secret: Secret )(using jsonStringSerializer: JsonStringSerializer): JwsSignature = ??? + override def validate(data: String, secret: Secret): Boolean = ??? + // TODO: Check implementation def sign( data: String, @@ -173,8 +150,6 @@ object Algorithm: rsaSignature.verify(Base64UrlDecoder.decode(signature)) } - override def validate(data: String, secret: Secret): Boolean = ??? - private def getPublicKey(str: String): PublicKey = { val pemParser = new PEMParser(new StringReader(str)) val keyPair = pemParser.readObject() @@ -201,27 +176,39 @@ object Algorithm: } } - /** Provides RSA algorithms. + /** HMAC using SHA-256. */ - object Rsa: // TODO: Rename + case object Hs256 extends Hmac: + override val hashingAlgorithmName = "HMACSHA256" - /** RSASSA-PKCS1-v1_5 using SHA-256. - */ - case object Rs256 extends Rsa: - override protected def hashingAlgorithmName: String = "SHA256withRSA" + /** HMAC using SHA-384. + */ + case object Hs384 extends Hmac: + override val hashingAlgorithmName = "HMACSHA384" - /** RSASSA-PKCS1-v1_5 using SHA-384. - */ - case object Rs384 extends Rsa: - override protected def hashingAlgorithmName: String = "SHA384withRSA" + /** HMAC using SHA-512. + */ + case object Hs512 extends Hmac: + override val hashingAlgorithmName = "HMACSHA512" + + /** RSASSA-PKCS1-v1_5 using SHA-256. + */ + case object Rs256 extends Rsa: + override protected def hashingAlgorithmName: String = "SHA256withRSA" - /** RSASSA-PKCS1-v1_5 using SHA-512. - */ - case object Rs512 extends Rsa: - override protected def hashingAlgorithmName: String = "SHA512withRSA" + /** RSASSA-PKCS1-v1_5 using SHA-384. + */ + case object Rs384 extends Rsa: + override protected def hashingAlgorithmName: String = "SHA384withRSA" + + /** RSASSA-PKCS1-v1_5 using SHA-512. + */ + case object Rs512 extends Rsa: + override protected def hashingAlgorithmName: String = "SHA512withRSA" // TODO: Docs. // TODO: Test. + // TODO: Move to the right place. def validate( data: String, secret: Secret // TODO: Do RSA algorithms use the same kind of secret, probably not? diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala index 9ad517f..139de02 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/headers/headers.scala @@ -1,14 +1,8 @@ package org.janjaali.sprayjwt -import org.janjaali.sprayjwt.algorithms.Algorithm +import org.janjaali.sprayjwt.algorithms.{Algorithm, Algorithms} import org.janjaali.sprayjwt.exceptions.{InvalidJwtAlgorithmException, InvalidJwtHeaderException} import spray.json.{JsObject, JsString, JsValue, JsonReader, JsonWriter} -import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs256 -import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs384 -import org.janjaali.sprayjwt.algorithms.Algorithm.Rsa.Rs512 -import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs256 -import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs384 -import org.janjaali.sprayjwt.algorithms.Algorithm.Hmac.Hs512 /** * Package object for headers package. @@ -23,15 +17,14 @@ package object headers { // TODO: Extract hardcoded strings out val algorithmIdentifier = jwtHeader.algorithm match { - case Rs256 => "RS256" - case Rs384 => "RS384" - case Rs512 => "RS512" - case Hs256 => "HS256" - case Hs384 => "HS384" - case Hs512 => "HS512" + case Algorithms.Rs256 => "RS256" + case Algorithms.Rs384 => "RS384" + case Algorithms.Rs512 => "RS512" + case Algorithms.Hs256 => "HS256" + case Algorithms.Hs384 => "HS384" + case Algorithms.Hs512 => "HS512" } - JsObject( "alg" -> JsString(algorithmIdentifier), "typ" -> JsString(jwtHeader.typ) @@ -48,12 +41,12 @@ package object headers { case Seq(JsString(alg), JsString(typ)) if typ == "JWT" => val maybeAlgorithm: Option[Algorithm] = alg match { - case "HS256" => Some(Algorithm.Hmac.Hs256) - case "HS384" => Some(Algorithm.Hmac.Hs384) - case "HS512" => Some(Algorithm.Hmac.Hs512) - case "RS256" => Some(Algorithm.Rsa.Rs256) - case "RS384" => Some(Algorithm.Rsa.Rs384) - case "RS512" => Some(Algorithm.Rsa.Rs512) + case "HS256" => Some(Algorithms.Hs256) + case "HS384" => Some(Algorithms.Hs384) + case "HS512" => Some(Algorithms.Hs512) + case "RS256" => Some(Algorithms.Rs256) + case "RS384" => Some(Algorithms.Rs384) + case "RS512" => Some(Algorithms.Rs512) } maybeAlgorithm match { diff --git a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala index 7df90b9..d93a643 100644 --- a/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala +++ b/spray-jwt/src/main/scala/org/janjaali/sprayjwt/jws/Header.scala @@ -1,6 +1,7 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.algorithms.Algorithms import org.janjaali.sprayjwt.json._ /** Represents a Header. @@ -65,24 +66,24 @@ object Header { new JsonWriter[algorithms.Algorithm] { override def write(algorithm: algorithms.Algorithm): JsonValue = algorithm match - case algorithms.Algorithm.Rsa.Rs256 => JsonString("RS256") - case algorithms.Algorithm.Rsa.Rs384 => JsonString("RS384") - case algorithms.Algorithm.Rsa.Rs512 => JsonString("RS512") - case algorithms.Algorithm.Hmac.Hs256 => JsonString("HS256") - case algorithms.Algorithm.Hmac.Hs384 => JsonString("HS384") - case algorithms.Algorithm.Hmac.Hs512 => JsonString("HS512") + case Algorithms.Rs256 => JsonString("RS256") + case Algorithms.Rs384 => JsonString("RS384") + case Algorithms.Rs512 => JsonString("RS512") + case Algorithms.Hs256 => JsonString("HS256") + case Algorithms.Hs384 => JsonString("HS384") + case Algorithms.Hs512 => JsonString("HS512") } } private[Header] def apply(algorithmName: String): Option[Algorithm] = algorithmName match - case "RS256" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs256)) - case "RS384" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs384)) - case "RS512" => Some(Algorithm(algorithms.Algorithm.Rsa.Rs512)) - case "HS256" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs256)) - case "HS384" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs384)) - case "HS512" => Some(Algorithm(algorithms.Algorithm.Hmac.Hs512)) + case "RS256" => Some(Algorithm(Algorithms.Rs256)) + case "RS384" => Some(Algorithm(Algorithms.Rs384)) + case "RS512" => Some(Algorithm(Algorithms.Rs512)) + case "HS256" => Some(Algorithm(Algorithms.Hs256)) + case "HS384" => Some(Algorithm(Algorithms.Hs384)) + case "HS512" => Some(Algorithm(Algorithms.Hs512)) case _ => None } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala index cacd7c4..c6f9c84 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/JsonSupportSpec.scala @@ -29,7 +29,7 @@ trait JsonSupportSpec extends ScalaTestSpec: private def verifySignWithHmac256Algorithm(): Unit = verifySignWithAlgorithm( - algorithm = Algorithm.Hmac.Hs256, + algorithm = Algorithms.Hs256, expectedSignature = JwsSignature( "jUzTJEnlFeTXDUPp9vJMwoalvXJ55IZ6DaBExN08UtA" ) @@ -37,7 +37,7 @@ trait JsonSupportSpec extends ScalaTestSpec: private def verifySignWithHmac384Algorithm(): Unit = verifySignWithAlgorithm( - algorithm = Algorithm.Hmac.Hs384, + algorithm = Algorithms.Hs384, expectedSignature = JwsSignature( "tz6NV8IfhPNqEnfUgeu0TJowwvWsjcmFCiRC_F-7bTOQeUle8jomj151nYHx1-IQ" ) @@ -45,7 +45,7 @@ trait JsonSupportSpec extends ScalaTestSpec: private def verifySignWithHmac512Algorithm(): Unit = verifySignWithAlgorithm( - algorithm = Algorithm.Hmac.Hs512, + algorithm = Algorithms.Hs512, expectedSignature = JwsSignature( "dOf7rSkv-y62jQDwAuzNdNKX2jfYK2HREBYqlB0rLnlERIlWkQ4BkVbbVyGi47br1Os4FllE4yjuz_FVjabK5w" ) @@ -114,5 +114,5 @@ trait JsonSupportSpec extends ScalaTestSpec: secret: Secret ): Unit = s"Verify deserializer with '$algorithmName'." in { - Algorithm.validate(data, secret) shouldBe true + Algorithms.validate(data, secret) shouldBe true } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala index 2aa9027..cb5e96e 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/algorithms/ScalaCheckGenerators.scala @@ -4,26 +4,14 @@ import org.scalacheck.Gen object ScalaCheckGenerators { - def rsaAlgorithmGen: Gen[Algorithm.Rsa] = { - Gen.oneOf( - Algorithm.Rsa.Rs256, - Algorithm.Rsa.Rs384, - Algorithm.Rsa.Rs512 - ) - } - - def hmacAlgorithmGen: Gen[Algorithm.Hmac] = { - Gen.oneOf( - Algorithm.Hmac.Hs256, - Algorithm.Hmac.Hs384, - Algorithm.Hmac.Hs512 - ) - } - def algorithmGen: Gen[Algorithm] = { Gen.oneOf( - hmacAlgorithmGen, - rsaAlgorithmGen + Algorithms.Hs256, + Algorithms.Hs384, + Algorithms.Hs512, + Algorithms.Rs256, + Algorithms.Rs384, + Algorithms.Rs512 ) } } diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala index 5b73a82..dc0afe1 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/headers/JwtHeaderJsonProtocolSpec.scala @@ -2,13 +2,13 @@ package org.janjaali.sprayjwt.headers import org.scalatest.funspec.AnyFunSpec import spray.json._ -import org.janjaali.sprayjwt.algorithms.Algorithm +import org.janjaali.sprayjwt.algorithms.Algorithms class JwtHeaderJsonProtocolSpec extends AnyFunSpec { describe("JwtHeaderJsonProtocol trait") { it("converts JWT-Header to JsValue") { - val jwtHeaderJson = JwtHeader(Algorithm.Hmac.Hs256).toJson + val jwtHeaderJson = JwtHeader(Algorithms.Hs256).toJson assert( jwtHeaderJson == JsObject( "typ" -> JsString("JWT"), diff --git a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala index 499c445..605f7aa 100644 --- a/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala +++ b/spray-jwt/src/test/scala/org/janjaali/sprayjwt/jws/HeaderSpec.scala @@ -1,6 +1,7 @@ package org.janjaali.sprayjwt.jws import org.janjaali.sprayjwt.algorithms +import org.janjaali.sprayjwt.algorithms.Algorithms import org.janjaali.sprayjwt.json.CommonJsonWriters.Implicits.jsonValueJsonWriter import org.janjaali.sprayjwt.json.{JsonBoolean, JsonString, JsonValue} import org.janjaali.sprayjwt.tests.ScalaCheckGeneratorsSampler._ @@ -48,7 +49,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("RS256"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs256) + expectedHeader = Header.Algorithm(Algorithms.Rs256) ) } @@ -56,7 +57,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("RS384"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs384) + expectedHeader = Header.Algorithm(Algorithms.Rs384) ) } @@ -64,7 +65,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("RS512"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Rsa.Rs512) + expectedHeader = Header.Algorithm(Algorithms.Rs512) ) } @@ -72,7 +73,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("HS256"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs256) + expectedHeader = Header.Algorithm(Algorithms.Hs256) ) } @@ -80,7 +81,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("HS384"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs384) + expectedHeader = Header.Algorithm(Algorithms.Hs384) ) } @@ -88,7 +89,7 @@ class HeaderSpec extends ScalaTestSpec with ScalaCheckDrivenPropertyChecks { behave like createAlgorithmHeader( value = JsonString("HS512"), - expectedHeader = Header.Algorithm(algorithms.Algorithm.Hmac.Hs512) + expectedHeader = Header.Algorithm(Algorithms.Hs512) ) }