Skip to content

Commit 683f1fd

Browse files
authored
Merge pull request #10 from scala-exercises/rr-endpoint-security
Prevent unauthorized access to the eval endpoint
2 parents e75cd74 + b79949f commit 683f1fd

File tree

5 files changed

+102
-13
lines changed

5 files changed

+102
-13
lines changed

build.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ lazy val evaluator = (project in file("."))
1717
"io.circe" %% "circe-core" % circeVersion,
1818
"io.circe" %% "circe-generic" % circeVersion,
1919
"io.circe" %% "circe-parser" % circeVersion,
20+
"com.typesafe" % "config" % "1.3.0",
21+
"com.pauldijou" %% "jwt-core" % "0.8.0",
2022
"org.log4s" %% "log4s" % "1.3.0",
2123
"org.slf4j" % "slf4j-simple" % "1.7.21",
2224
"io.get-coursier" %% "coursier" % "1.0.0-M12",

src/main/resources/application.conf

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
eval.auth {
2+
secretKey = "secretKey"
3+
secretKey = ${?EVAL_SECRET_KEY}
4+
}
5+

src/main/scala/auth.scala

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.scalaexercises.evaluator
2+
3+
import org.http4s._, org.http4s.dsl._, org.http4s.server._
4+
import com.typesafe.config._
5+
import org.http4s.util._
6+
import scala.util.{Try, Success, Failure}
7+
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}
8+
9+
import scalaz.concurrent.Task
10+
11+
object auth {
12+
13+
val config = ConfigFactory.load()
14+
15+
val SecretKeyPath = "eval.auth.secretKey"
16+
17+
val secretKey = if (config.hasPath(SecretKeyPath)) {
18+
config.getString(SecretKeyPath)
19+
} else {
20+
throw new IllegalStateException("Missing -Deval.auth.secretKey=[YOUR_KEY_HERE] or env var [EVAL_SECRET_KEY] ")
21+
}
22+
23+
object `X-Scala-Eval-Api-Token` extends HeaderKey.Singleton {
24+
25+
type HeaderT = `X-Scala-Eval-Api-Token`
26+
27+
def name: CaseInsensitiveString = "x-scala-eval-api-token".ci
28+
29+
override def parse(s: String): ParseResult[`X-Scala-Eval-Api-Token`] =
30+
ParseResult.success(`X-Scala-Eval-Api-Token`(s))
31+
32+
def matchHeader(header: Header): Option[HeaderT] = {
33+
if (header.name == name) Some(`X-Scala-Eval-Api-Token`(header.value))
34+
else None
35+
}
36+
37+
}
38+
39+
final case class `X-Scala-Eval-Api-Token`(token: String) extends Header.Parsed {
40+
override def key = `X-Scala-Eval-Api-Token`
41+
override def renderValue(writer: Writer): writer.type =
42+
writer.append(token)
43+
}
44+
45+
def apply(service: HttpService): HttpService = Service.lift { req =>
46+
req.headers.get(`X-Scala-Eval-Api-Token`) match {
47+
case Some(header) =>
48+
Jwt.decodeRaw(header.value, secretKey, Seq(JwtAlgorithm.HS256)) match {
49+
case Success(_) => service(req)
50+
case Failure(_) => Task.now(Response(Status.Unauthorized))
51+
}
52+
case None => Task.now(Response(Status.Unauthorized))
53+
}
54+
55+
}
56+
57+
}

src/main/scala/services.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ object services {
2424

2525
val evaluator = new Evaluator(20 seconds)
2626

27-
def service = HttpService {
27+
def evalService = auth(HttpService {
2828
case req @ POST -> Root / "eval" =>
2929
import io.circe.syntax._
3030
req.decode[EvalRequest] { evalRequest =>
@@ -45,7 +45,7 @@ object services {
4545
Ok(response.asJson)
4646
}
4747
}
48-
}
48+
})
4949

5050
}
5151

@@ -66,7 +66,7 @@ object EvaluatorServer extends App {
6666

6767
BlazeBuilder
6868
.bindHttp(port, ip)
69-
.mountService(service)
69+
.mountService(evalService)
7070
.start
7171
.run
7272
.awaitShutdown()

src/test/scala/EvalEndpointSpec.scala

+35-10
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,43 @@
55
package org.scalaexercises.evaluator
66

77
import org.scalatest._
8-
import org.http4s._, org.http4s.dsl._, org.http4s.server._
8+
import org.http4s._
9+
import org.http4s.headers._
10+
import org.http4s.dsl._
11+
import org.http4s.server._
912

1013
import io.circe.syntax._
1114
import io.circe.generic.auto._
1215
import scalaz.stream.Process.emit
1316
import java.nio.charset.StandardCharsets
1417
import scodec.bits.ByteVector
18+
import pdi.jwt.{Jwt, JwtAlgorithm, JwtHeader, JwtClaim, JwtOptions}
1519

1620
import org.http4s.{Status => HttpStatus}
1721

1822
class EvalEndpointSpec extends FunSpec with Matchers {
1923

2024
import services._
2125
import codecs._
26+
import auth._
2227
import EvalResponse.messages._
2328

2429
val sonatypeReleases = "https://oss.sonatype.org/content/repositories/releases/" :: Nil
2530

26-
def serve(evalRequest: EvalRequest) =
27-
service.run(Request(
31+
val validToken = Jwt.encode("""{"user": "scala-exercises"}""", auth.secretKey, JwtAlgorithm.HS256)
32+
33+
val invalidToken = java.util.UUID.randomUUID.toString
34+
35+
def serve(evalRequest: EvalRequest, authHeader : Header) =
36+
evalService.run(Request(
2837
POST,
2938
Uri(path = "/eval"),
3039
body = emit(
3140
ByteVector.view(
3241
evalRequest.asJson.noSpaces.getBytes(StandardCharsets.UTF_8)
3342
)
3443
)
35-
)).run
44+
).putHeaders(authHeader)).run
3645

3746
def verifyEvalResponse(
3847
response: Response,
@@ -50,7 +59,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
5059
describe("evaluation") {
5160
it("can evaluate simple expressions") {
5261
verifyEvalResponse(
53-
response = serve(EvalRequest(code = "{ 41 + 1 }")),
62+
response = serve(EvalRequest(code = "{ 41 + 1 }"), `X-Scala-Eval-Api-Token`(validToken)),
5463
expectedStatus = HttpStatus.Ok,
5564
expectedValue = Some("42"),
5665
expectedMessage = `ok`
@@ -59,7 +68,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
5968

6069
it("fails with a timeout when takes longer than the configured timeout") {
6170
verifyEvalResponse(
62-
response = serve(EvalRequest(code = "{ while(true) {}; 123 }")),
71+
response = serve(EvalRequest(code = "{ while(true) {}; 123 }"), `X-Scala-Eval-Api-Token`(validToken)),
6372
expectedStatus = HttpStatus.Ok,
6473
expectedValue = None,
6574
expectedMessage = `Timeout Exceded`
@@ -72,7 +81,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
7281
code = "{import cats._; Eval.now(42).value}",
7382
resolvers = sonatypeReleases,
7483
dependencies = Dependency("org.typelevel", "cats_2.11", "0.6.0") :: Nil
75-
)),
84+
), `X-Scala-Eval-Api-Token`(validToken)),
7685
expectedStatus = HttpStatus.Ok,
7786
expectedValue = Some("42"),
7887
expectedMessage = `ok`
@@ -89,7 +98,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
8998
code = code,
9099
resolvers = resolvers,
91100
dependencies = Dependency("org.typelevel", "cats_2.11", version) :: Nil
92-
)),
101+
), `X-Scala-Eval-Api-Token`(validToken)),
93102
expectedStatus = HttpStatus.Ok,
94103
expectedValue = Some("42"),
95104
expectedMessage = `ok`
@@ -104,7 +113,7 @@ class EvalEndpointSpec extends FunSpec with Matchers {
104113
code = "{import stdlib._; Asserts.scalaTestAsserts(true)}",
105114
resolvers = sonatypeReleases,
106115
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
107-
)),
116+
), `X-Scala-Eval-Api-Token`(validToken)),
108117
expectedStatus = HttpStatus.Ok,
109118
expectedValue = Some("()"),
110119
expectedMessage = `ok`
@@ -117,13 +126,29 @@ class EvalEndpointSpec extends FunSpec with Matchers {
117126
code = "{import stdlib._; Asserts.scalaTestAsserts(false)}",
118127
resolvers = sonatypeReleases,
119128
dependencies = Dependency("org.scala-exercises", "exercises-stdlib_2.11", "0.2.0") :: Nil
120-
)),
129+
), `X-Scala-Eval-Api-Token`(validToken)),
121130
expectedStatus = HttpStatus.Ok,
122131
expectedValue = None,
123132
expectedMessage = `Runtime Error`
124133
)
125134
}
126135

136+
it("rejects requests with invalid tokens") {
137+
serve(EvalRequest(
138+
code = "1",
139+
resolvers = Nil,
140+
dependencies = Nil
141+
), `X-Scala-Eval-Api-Token`(invalidToken)).status should be (HttpStatus.Unauthorized)
142+
}
143+
144+
it("rejects requests with missing tokens") {
145+
serve(EvalRequest(
146+
code = "1",
147+
resolvers = Nil,
148+
dependencies = Nil
149+
), `Accept-Ranges`(Nil)).status should be (HttpStatus.Unauthorized)
150+
}
151+
127152
}
128153
}
129154

0 commit comments

Comments
 (0)