From 51f9bdcd6bbed42b3244f641e58b159691ca13f8 Mon Sep 17 00:00:00 2001 From: Simon Richard Date: Tue, 3 Mar 2020 22:13:52 -0500 Subject: [PATCH] Implement simple authentication with JWTs --- build.sbt | 3 +- src/main/resources/application.conf | 4 ++ .../sylvansson/socialnetwork/Endpoints.scala | 46 +++++++++++++------ .../sylvansson/socialnetwork/Exceptions.scala | 5 ++ .../sylvansson/socialnetwork/Responses.scala | 9 +++- .../sylvansson/socialnetwork/Service.scala | 3 ++ 6 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 src/main/scala/com/github/sylvansson/socialnetwork/Exceptions.scala diff --git a/build.sbt b/build.sbt index d767e4e..0d0ef2d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,12 +1,13 @@ name := "socialnetwork" -version := "0.2" +version := "0.3" scalaVersion := "2.12.10" libraryDependencies ++= Seq( "com.github.finagle" %% "finch-core" % "0.31.0", "com.github.finagle" %% "finch-circe" % "0.31.0", + "com.pauldijou" %% "jwt-circe" % "4.2.0", "io.circe" %% "circe-generic" % "0.13.0", "io.getquill" %% "quill-jdbc" % "3.5.0", "org.postgresql" % "postgresql" % "42.2.10", diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index fa0b704..7cdb1ce 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -5,3 +5,7 @@ jdbc { dataSource.portNumber = 5432 dataSource.serverName = "localhost" } + +jwt { + secret = "correcthorsebatterystaple" +} diff --git a/src/main/scala/com/github/sylvansson/socialnetwork/Endpoints.scala b/src/main/scala/com/github/sylvansson/socialnetwork/Endpoints.scala index fff42e0..d790611 100644 --- a/src/main/scala/com/github/sylvansson/socialnetwork/Endpoints.scala +++ b/src/main/scala/com/github/sylvansson/socialnetwork/Endpoints.scala @@ -1,35 +1,55 @@ package com.github.sylvansson.socialnetwork + import java.util.UUID +import com.github.sylvansson.socialnetwork.Exceptions.AuthenticationError import com.github.sylvansson.socialnetwork.Responses._ import com.twitter.util.Future +import com.typesafe.config.Config import io.circe.generic.auto._ +import io.finch._ import io.finch.circe._ -import io.finch.syntax.{get, post} -import io.finch.{Endpoint, Ok, param, _} +import io.finch.syntax._ +import pdi.jwt._ object Endpoints { - def service = friendships.toService + def service(implicit config: Config) = friendships.toService + + /** + * Validate the caller's token and extract their user id. + * @return The user's id. + */ + def authorize(implicit config: Config): Endpoint[UUID] = { + header("Authorization").mapOutput({ + case header if header.startsWith("Bearer") => + val _ :: token :: Nil = header.split(" ").toList + val key = config.getString("jwt.secret") + JwtCirce.decodeJson(token, key, Seq(JwtAlgorithm.HS256)) + .toOption + .map(_.hcursor.get[UUID]("userId")) match { + case Some(Right(userId)) => Ok(userId) + case _ => BadRequest(new AuthenticationError) + } + }) + } - private def friendships = { + private def friendships(implicit config: Config) = { val listAcceptedFriendships: Endpoint[Success[Seq[Friendship]]] = - get("friendships.accepted" :: param[UUID]("user")) { userId: UUID => - Future(Friendship.findAccepted(userId)) + get("friendships.accepted" :: authorize) { callerId: UUID => + Future(Friendship.findAccepted(callerId)) .map(fs => Ok(Success("acceptedFriendships", fs))) } val listPendingFriendships: Endpoint[Success[Seq[Friendship]]] = - get("friendships.pending" :: param[UUID]("user")) { userId: UUID => - Future(Friendship.findPending(userId)) + get("friendships.pending" :: authorize) { callerId: UUID => + Future(Friendship.findPending(callerId)) .map(fs => Ok(Success("pendingFriendships", fs))) } - // TODO: Ensure that only the requestee can accept a friendship, - // once authentication has been implemented. val acceptFriendship: Endpoint[EmptySuccess] = - post("friendships.accept" :: param[UUID]("requester") :: param[UUID]("requestee")) { - (requesterId: UUID, requesteeId: UUID) => - Future(Friendship.accept(requesterId, requesteeId)) + post("friendships.accept" :: param[UUID]("requester") :: authorize) { + (requesterId: UUID, callerId: UUID) => + Future(Friendship.accept(requesterId, callerId)) .map(_ => Ok(EmptySuccess())) } diff --git a/src/main/scala/com/github/sylvansson/socialnetwork/Exceptions.scala b/src/main/scala/com/github/sylvansson/socialnetwork/Exceptions.scala new file mode 100644 index 0000000..1414a2a --- /dev/null +++ b/src/main/scala/com/github/sylvansson/socialnetwork/Exceptions.scala @@ -0,0 +1,5 @@ +package com.github.sylvansson.socialnetwork + +object Exceptions { + class AuthenticationError extends Exception("invalid_token") +} diff --git a/src/main/scala/com/github/sylvansson/socialnetwork/Responses.scala b/src/main/scala/com/github/sylvansson/socialnetwork/Responses.scala index ed3bef3..f5f1523 100644 --- a/src/main/scala/com/github/sylvansson/socialnetwork/Responses.scala +++ b/src/main/scala/com/github/sylvansson/socialnetwork/Responses.scala @@ -19,7 +19,14 @@ object Responses { implicit def encode[T](implicit encodeT: Encoder[T]): Encoder[Success[T]] = (s: Success[T]) => Json.obj( "ok" -> Json.fromBoolean(true), - s.property -> s.data.asJson + s.property -> s.data.asJson, ) } + + implicit val encodeException: Encoder[Exception] = Encoder.instance { e: Exception => + Json.obj( + "ok" -> Json.fromBoolean(false), + "error" -> Json.fromString(e.getMessage), + ) + } } diff --git a/src/main/scala/com/github/sylvansson/socialnetwork/Service.scala b/src/main/scala/com/github/sylvansson/socialnetwork/Service.scala index 5d55dca..a498d55 100644 --- a/src/main/scala/com/github/sylvansson/socialnetwork/Service.scala +++ b/src/main/scala/com/github/sylvansson/socialnetwork/Service.scala @@ -3,8 +3,11 @@ package com.github.sylvansson.socialnetwork import com.github.sylvansson.socialnetwork.Endpoints._ import com.twitter.finagle.Http import com.twitter.util.Await +import com.typesafe.config.ConfigFactory object Service extends App { + implicit val config = ConfigFactory.load + Await.ready( Http.serve(":8080", service) )