Skip to content

Commit

Permalink
Init project
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldijou committed Sep 8, 2014
0 parents commit 57c732a
Show file tree
Hide file tree
Showing 27 changed files with 825 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target
project/project
project/target
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Scala JWT

JWT support for Scala. Pick the best project depending on your need...

## Core

Low-level API based mostly based on String and Map since there is no native support for JSON in Scala.

## Play JSON

Nice API to interact with JWT using JsObject from the Play JSON lib.

## Play

Built in top of Play JSON, extend the `Result` class in order to allow you to manage the `Session` using JWT.

## Json4s

Work in progress...
66 changes: 66 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import sbt._
import Keys._
import Dependencies._

object ProjectBuild extends Build {
lazy val Java8 = config("java8").extend( Compile )
lazy val JavaLegacy = config("javaLegacy").extend( Compile )

val CommonSettings = Defaults.defaultSettings ++ Seq(
organization := "pdi",
version := "0.1.0",
scalaVersion := "2.11.2",
sourcesInBase := false,
// What I would like to do...
// unmanagedSourceDirectories in Java8 += baseDirectory.value / "src/main/scala-java-8",
// unmanagedSourceDirectories in JavaLegacy += baseDirectory.value / "src/main/scala-java-legacy",
// But it failed, so I ended up with that:
unmanagedSourceDirectories in Compile += baseDirectory.value / "src/main/scala-java-8",
libraryDependencies ++= Seq(scalatest)
)

lazy val coreProject = Project("core", file("scala-jwt-core"))
.configs(Java8, JavaLegacy)
.settings(CommonSettings: _*)
.settings( inConfig(Java8)(Defaults.configTasks): _* )
.settings( inConfig(JavaLegacy)(Defaults.configTasks): _* )
.settings(
name := "core"
)

lazy val json4sNativeProject = Project("json4s", file("scala-jwt-json4s"))
.settings(CommonSettings: _*)
.settings(
name := "json4s",
libraryDependencies ++= Seq(json4sNative)
)
.aggregate(coreProject)
.dependsOn(coreProject)

lazy val json4sJacksonProject = Project("json4s", file("scala-jwt-json4s"))
.settings(CommonSettings: _*)
.settings(
name := "json4s",
libraryDependencies ++= Seq(json4sJackson)
)
.aggregate(coreProject)
.dependsOn(coreProject)

lazy val playJsonProject = Project("play-json", file("scala-jwt-play-json"))
.settings(CommonSettings: _*)
.settings(
name := "play-json",
libraryDependencies ++= Seq(playJson)
)
.aggregate(coreProject)
.dependsOn(coreProject)

lazy val playProject = Project("play", file("scala-jwt-play"))
.settings(CommonSettings: _*)
.settings(
name := "play",
libraryDependencies ++= Seq(play)
)
.aggregate(playJsonProject)
.dependsOn(playJsonProject)
}
17 changes: 17 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sbt._

object Dependencies {
object V {
val play = "2.3.3"
val json4s = "3.2.10"
val scalatest = "2.2.1"
}

val play = "com.typesafe.play" %% "play" % V.play
val playJson = "com.typesafe.play" %% "play-json" % V.play

val json4sNative = "org.json4s" %% "json4s-native" % V.json4s
val json4sJackson = "org.json4s" %% "json4s-jackson" % V.json4s

val scalatest = "org.scalatest" % "scalatest_2.11" % V.scalatest % "test"
}
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.5
9 changes: 9 additions & 0 deletions scala-jwt-core/src/main/scala-java-8/JwtBase64Impl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pdi.scala.jwt

trait JwtBase64Impl {
private lazy val encoder = java.util.Base64.getUrlEncoder()
private lazy val decoder = java.util.Base64.getUrlDecoder()

def encode(value: Array[Byte]): Array[Byte] = encoder.encode(value)
def decode(value: Array[Byte]): Array[Byte] = decoder.decode(value)
}
25 changes: 25 additions & 0 deletions scala-jwt-core/src/main/scala-java-8/JwtTimeImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pdi.scala.jwt

import java.time.Instant

trait JwtTimeImpl {
def now: Long = Instant.now().toEpochMilli

def nowIsBetween(start: Option[Long], end: Option[Long]): Boolean = {
val timeNow = now
start.map(_ <= timeNow).getOrElse(true) && end.map(_ >= timeNow).getOrElse(true)
}

def validateNowIsBetween(start: Option[Long], end: Option[Long]): Boolean = {
val timeNow = now
start.map(notBefore => if (timeNow < notBefore) {
throw new JwtNotBeforeException("", notBefore)
} else {
true
}).getOrElse(true) && end.map(expiration => if (timeNow > expiration) {
throw new JwtExpirationException("", expiration)
} else {
true
}).getOrElse(true)
}
}
10 changes: 10 additions & 0 deletions scala-jwt-core/src/main/scala-java-legacy/JwtBase64Impl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pdi.scala.jwt

import org.apache.commons.codec.binary.Base64

trait JwtBase64Impl {
val codec = new Base64(true)

def encode(value: Array[Byte]): Array[Byte] = codec.encode(value))
def decode(value: Array[Byte]): Array[Byte] = codec.encode(value)
}
14 changes: 14 additions & 0 deletions scala-jwt-core/src/main/scala-java-legacy/JwtTimeImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package pdi.scala.jwt

java.util.Calendar

trait JwtTimeImpl {
val TimeZoneUTC = TimeZone.getTimeZone("UTC")

def now: Long = Calendar.getInstance(TimeZoneUTC).getTimeInMillis

def nowIsBetween(start: Option[Long], end: Option[Long]): Boolean = {
val timeNow = now
start.map(_ <= timeNow).getOrElse(true) && end.map(_ >= timeNow).getOrElse(true)
}
}
84 changes: 84 additions & 0 deletions scala-jwt-core/src/main/scala/Jwt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package pdi.scala.jwt

import scala.util.Try

object Jwt extends JwtCore[String, String] {
def decodeAll(token: String, maybeKey: Option[String] = None): Try[(String, String, Option[String])] =
decodeRawAllValidated(token, maybeKey)
}

trait JwtCore[H, C] {
// Encode
def encode(header: String, claim: String, key: Option[String] = None, algorithm: Option[String] = None): String = {
val header64 = JwtBase64.encodeString(header)
val claim64 = JwtBase64.encodeString(claim)
Seq(
header64,
claim64,
JwtBase64.encodeString(JwtUtils.sign(header64 + "." + claim64, key, algorithm))
).mkString(".")
}

def encode(header: String, claim: String, key: String, algorithm: String): String =
encode(header, claim, Option(key), Option(algorithm))

def encode(header: JwtHeader, claim: JwtClaim): String =
encode(header.toJson, claim.toJson, None, None)

def encode(header: JwtHeader, claim: JwtClaim, key: String): String =
encode(header.toJson, claim.toJson, Option(key), header.algorithm)

// Decode
def decodeRawAll(token: String): Try[(String, String, Option[String])] = Try {
val parts = token.split("\\.")

parts.length match {
case 2 => (JwtBase64.decodeString(parts(0)), JwtBase64.decodeString(parts(1)), None)
case 3 => (JwtBase64.decodeString(parts(0)), JwtBase64.decodeString(parts(1)), Option(parts(2)))
case _ => throw new JwtLengthException(s"Expected token [$token] to be composed of 2 or 3 parts separated by dots.")
}
}

def decodeRaw(token: String): Try[String] = decodeRawAll(token).map(_._2)

protected def decodeRawAllValidated(token: String, maybeKey: Option[String] = None): Try[(String, String, Option[String])] =
Try {
if (validate(token, maybeKey)) {
decodeRawAll(token).get
} else {
throw JwtValidationException
}
}

def decodeAll(token: String, maybeKey: Option[String] = None): Try[(H, C, Option[String])]

def decodeAll(token: String, key: String): Try[(H, C, Option[String])] = decodeAll(token, Option(key))

def decode(token: String, maybeKey: Option[String] = None): Try[C] = decodeAll(token, maybeKey).map(_._2)

def decode(token: String, key: String): Try[C] = decode(token, Option(key))

// Validate
private val extractAlgorithmRegex = "\"alg\":\"([a-zA-Z0-9]+)\"".r
private def extractAlgorithm(header64: String): Option[String] = for {
extractAlgorithmRegex(algo) <- extractAlgorithmRegex findFirstIn JwtBase64.decodeString(header64)
} yield algo

def validate(token: String, maybeKey: Option[String] = None): Boolean = {
val parts = token.split("\\.")
val maybeAlgo = if (parts.length > 0) { extractAlgorithm(parts(0)) } else { None }

parts.length match {
// No signature => no algo in header and no key
case 2 => maybeKey.isEmpty && maybeAlgo.isEmpty
// Same as 2
case 3 if parts(2).isEmpty => maybeKey.isEmpty && maybeAlgo.isEmpty
// Signature => need to match
case 3 => parts(2) == JwtBase64.encodeString(JwtUtils.sign(parts(0) +"."+ parts(1), maybeKey, maybeAlgo))
// WTF?
case _ => false
}
}

def validate(token: String, key: String): Boolean = validate(token, Option(key))
}
12 changes: 12 additions & 0 deletions scala-jwt-core/src/main/scala/JwtBase64.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pdi.scala.jwt

object JwtBase64 extends JwtBase64Impl {
def encode(value: String): Array[Byte] = encode(JwtUtils.bytify(value))
def decode(value: String): Array[Byte] = decode(JwtUtils.bytify(value))

def encodeString(value: Array[Byte]): String = JwtUtils.stringify(encode(value))
def decodeString(value: Array[Byte]): String = JwtUtils.stringify(decode(value))

def encodeString(value: String): String = JwtUtils.stringify(encode(value))
def decodeString(value: String): String = JwtUtils.stringify(decode(value))
}
50 changes: 50 additions & 0 deletions scala-jwt-core/src/main/scala/JwtClaim.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package pdi.scala.jwt

case class JwtClaim(
content: String = "{}",
issuer: Option[String] = None,
subject: Option[String] = None,
audience: Option[String] = None,
expiration: Option[Long] = None,
notBefore: Option[Long] = None,
issuedAt: Option[Long] = None,
jwtId: Option[String] = None
) {
def toJson: String = JwtUtils.mergeJson(content, JwtUtils.mapToJson(Map(
"iss" -> issuer,
"sub" -> subject,
"aud" -> audience,
"exp" -> expiration,
"nbf" -> notBefore,
"iat" -> issuedAt,
"jti" -> jwtId
).collect {
case (key, Some(value)) => (key -> value)
}))
def + (json: String*): JwtClaim = this.copy(content = JwtUtils.mergeJson(this.content, json: _*))

/*def + (fields: (String, Any)*): JwtClaim =
this.copy(content = JwtUtils.mergeJson(this.content, JwtUtils.mapToJson(fields.toMap)))*/

def by(issuer: String): JwtClaim = this.copy(issuer = Option(issuer))

def to(audience: String): JwtClaim = this.copy(audience = Option(audience))

def about(subject: String): JwtClaim = this.copy(subject = Option(subject))

def withId(id: String): JwtClaim = this.copy(jwtId = Option(id))

def expiresIn(millis: Long): JwtClaim =
this.copy(expiration = this.expiration.map(_ + millis).orElse(Option(JwtTime.now + millis)))

def expiresAt(millis: Long): JwtClaim = this.copy(expiration = Option(millis))

def startsIn(millis: Long): JwtClaim =
this.copy(notBefore = this.notBefore.map(_ + millis).orElse(Option(JwtTime.now + millis)))

def startsAt(millis: Long): JwtClaim = this.copy(notBefore = Option(millis))

def issuedNow: JwtClaim = this.copy(issuedAt = Option(JwtTime.now))

def isValid: Boolean = JwtTime.nowIsBetween(this.notBefore, this.expiration)
}
11 changes: 11 additions & 0 deletions scala-jwt-core/src/main/scala/JwtException.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pdi.scala.jwt

sealed trait JwtException

class JwtLengthException(message: String) extends RuntimeException(message) with JwtException

object JwtValidationException extends RuntimeException with JwtException

class JwtExpirationException(message: String, expiration: Long) extends RuntimeException(message) with JwtException

class JwtNotBeforeException(message: String, notBefore: Long) extends RuntimeException(message) with JwtException
15 changes: 15 additions & 0 deletions scala-jwt-core/src/main/scala/JwtHeader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pdi.scala.jwt

case class JwtHeader(
algorithm: Option[String] = None,
typ: Option[String] = None,
contentType: Option[String] = None
) {
def toJson: String = JwtUtils.mapToJson(Map(
"alg" -> algorithm,
"typ" -> typ,
"cty" -> contentType
).collect {
case (key, Some(value)) => (key -> value)
})
}
3 changes: 3 additions & 0 deletions scala-jwt-core/src/main/scala/JwtTime.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package pdi.scala.jwt

object JwtTime extends JwtTimeImpl {}
Loading

0 comments on commit 57c732a

Please sign in to comment.