Skip to content

Commit

Permalink
Implement simple authentication with JWTs
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvansson committed Mar 4, 2020
1 parent 16291ea commit 51f9bdc
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 15 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ jdbc {
dataSource.portNumber = 5432
dataSource.serverName = "localhost"
}

jwt {
secret = "correcthorsebatterystaple"
}
46 changes: 33 additions & 13 deletions src/main/scala/com/github/sylvansson/socialnetwork/Endpoints.scala
Original file line number Diff line number Diff line change
@@ -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()))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.sylvansson.socialnetwork

object Exceptions {
class AuthenticationError extends Exception("invalid_token")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down

0 comments on commit 51f9bdc

Please sign in to comment.