auth API routes, add logger

This commit is contained in:
Will Freeman
2024-12-09 20:55:53 -07:00
parent c489d7dbae
commit e8e186c4c3
6 changed files with 132 additions and 22 deletions

View File

@@ -1 +1 @@
{"name":"sbt","version":"1.9.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%252B7/OpenJDK17U-jdk_x64_mac_hotspot_17.0.10_7.tar.gz/jdk-17.0.10+7/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/sbt/sbt/releases/download/v1.8.2/sbt-1.8.2.zip/sbt/bin/sbt-launch.jar","-Dsbt.script=/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/sbt/sbt/releases/download/v1.8.2/sbt-1.8.2.zip/sbt/bin/sbt","xsbt.boot.Boot","-bsp"]}
{"name":"sbt","version":"1.9.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/willfreeman/Library/Java/JavaVirtualMachines/corretto-11.0.19/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/willfreeman/Library/Application Support/JetBrains/IdeaIC2024.1/plugins/Scala/launcher/sbt-launch.jar","-Dsbt.script=/Users/willfreeman/Library/Application%20Support/Coursier/bin/sbt","xsbt.boot.Boot","-bsp"]}

View File

@@ -14,6 +14,10 @@ lazy val root = (project in file("."))
val PekkoVersion = "1.0.3"
val PekkoHttpVersion = "1.0.1"
libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.5.6",
"org.slf4j" % "slf4j-api" % "2.0.12",
"com.auth0" % "jwks-rsa" % "0.22.1",
"com.github.jwt-scala" % "jwt-json-common_native0.4_2.12" % "10.0.1",
"org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion,
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<logger name="services.JwtAuthenticator" level="INFO" />
</configuration>

View File

@@ -14,6 +14,7 @@ import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import org.apache.pekko.http.scaladsl.server.RejectionHandler
import services.DeleteObjectRequest
import services.DeleteObjectRequestJsonProtocol._
import services.JwtAuthenticator.{authenticated, hasPermissions}
import spray.json.DefaultJsonProtocol._
import services.ImageSubmissionJsonProtocol._
@@ -21,11 +22,14 @@ import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import scala.concurrent.ExecutionContextExecutor
import scala.io.StdIn
import org.slf4j.LoggerFactory
object ShotgunServer {
def main(args: Array[String]): Unit = {
val logger = LoggerFactory.getLogger(getClass)
implicit val system: ActorSystem = ActorSystem("my-system")
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
val logging = Logging(system, getClass)
@@ -82,35 +86,34 @@ object ShotgunServer {
}
},
path("presigned-urls") {
get {
parameters("count".as[Int], "contentType".as[String], "author".as[String]) { (imageCount, contentType, author) =>
if (imageCount > 5)
complete(StatusCodes.BadRequest, "Cannot request more than 5 presigned URLs at a time")
else {
val urls = awsClient.getMultiplePutPresignedUrls("deflock-photo-uploads", imageCount, contentType, author, 5)
complete(urls)
hasPermissions(List("photo:upload")) { claim =>
get {
parameters("count".as[Int], "contentType".as[String], "author".as[String]) { (imageCount, contentType, author) =>
if (imageCount > 5)
complete(StatusCodes.BadRequest, "Cannot request more than 5 presigned URLs at a time")
else {
val urls = awsClient.getMultiplePutPresignedUrls("deflock-photo-uploads", imageCount, contentType, author, 5)
complete(urls)
}
}
}
}
},
path("user-submissions") {
get {
val submissions = awsClient.getAllObjects("deflock-photo-uploads")
complete(submissions)
}
},
path("delete-object") {
post {
entity(as[DeleteObjectRequest]) { request =>
val res = awsClient.deleteObject("deflock-photo-uploads", request.objectKey)
complete(res)
hasPermissions(List("photo:view")) { claim =>
get {
val submissions = awsClient.getAllObjects("deflock-photo-uploads")
complete(submissions)
}
}
},
path("oauth2" / "callback") {
get {
parameters(Symbol("code").?) { (code) =>
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to Pekko HTTP</h1><p><b>Code: " + code.getOrElse("None") + "</b></p>"))
path("delete-object") {
hasPermissions(List("photo:delete")) { claim =>
post {
entity(as[DeleteObjectRequest]) { request =>
val res = awsClient.deleteObject("deflock-photo-uploads", request.objectKey)
complete(res)
}
}
}
}

View File

@@ -0,0 +1,88 @@
package services
import org.apache.pekko.http.scaladsl.server.{Directive1, Directives}
import pdi.jwt.{Jwt, JwtAlgorithm, JwtOptions}
import spray.json.JsValue
import spray.json.JsonParser
import com.auth0.jwk.{JwkProvider, JwkProviderBuilder}
import java.security.PublicKey
import java.security.interfaces.RSAPublicKey
import scala.util.{Failure, Success, Try}
import java.util.concurrent.TimeUnit
import org.slf4j.{Logger, LoggerFactory}
import java.util.Base64
object JwtAuthenticator extends Directives {
val logger: Logger = LoggerFactory.getLogger(getClass)
val domain = "https://deflock.us.auth0.com"
val jwkProvider: JwkProvider = new JwkProviderBuilder(domain)
.cached(10, 24, TimeUnit.HOURS) // Cache up to 10 keys for 24 hours
.build()
def getPublicKey(kid: String): Option[PublicKey] = {
Try {
val jwk = jwkProvider.get(kid)
jwk.getPublicKey.asInstanceOf[RSAPublicKey]
} match {
case Success(publicKey) => Some(publicKey)
case Failure(_) => None
}
}
def validateToken(token: String): Option[JsValue] = {
val parts = token.split("\\.")
if (parts.length != 3) {
return None
}
val rawHeader = new String(Base64.getUrlDecoder.decode(parts(0)))
val headerJson = JsonParser(rawHeader)
val kid = headerJson.asJsObject.fields.get("kid").flatMap {
case spray.json.JsString(k) => Some(k)
case _ => None
}
kid.flatMap(getPublicKey) match {
case Some(publicKey) =>
Jwt.decode(token, publicKey, Seq(JwtAlgorithm.RS256), JwtOptions.DEFAULT) match {
case Success(claim) =>
Some(JsonParser(claim.content))
case Failure(_) =>
None
}
case None =>
None
}
}
def authenticated: Directive1[JsValue] = {
optionalHeaderValueByName("Authorization").flatMap {
case Some(header) if header.startsWith("Bearer ") =>
val token = header.substring("Bearer ".length)
validateToken(token) match {
case Some(claim) => provide(claim)
case None => complete((401, "Invalid token"))
}
case _ =>
complete((401, "Missing or malformed Authorization header"))
}
}
def hasPermissions(requiredPermissions: List[String]): Directive1[JsValue] = {
authenticated.flatMap { claim =>
val permissions = claim.asJsObject.fields.get("permissions").flatMap {
case spray.json.JsArray(elements) => Some(elements.collect { case spray.json.JsString(permission) => permission })
case _ => None
}
permissions match {
case Some(userPermissions) if requiredPermissions.forall(userPermissions.contains) =>
provide(claim)
case _ =>
complete((403, "Insufficient permissions"))
}
}
}
}

View File

@@ -16,6 +16,7 @@ const auth0 = createAuth0({
clientId: "IEBa7ckgWrMGErTWXA8Z9q91hre7uII2",
authorizationParams: {
redirect_uri: window.location.origin,
audience: "https://deflock.me/api",
},
cacheLocation: 'localstorage',
})