mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-09-01 23:20:44 +02:00
auth API routes, add logger
This commit is contained in:
@@ -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"]}
|
||||||
@@ -14,6 +14,10 @@ lazy val root = (project in file("."))
|
|||||||
val PekkoVersion = "1.0.3"
|
val PekkoVersion = "1.0.3"
|
||||||
val PekkoHttpVersion = "1.0.1"
|
val PekkoHttpVersion = "1.0.1"
|
||||||
libraryDependencies ++= Seq(
|
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-actor-typed" % PekkoVersion,
|
||||||
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
|
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
|
||||||
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
|
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -14,6 +14,7 @@ import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
|||||||
import org.apache.pekko.http.scaladsl.server.RejectionHandler
|
import org.apache.pekko.http.scaladsl.server.RejectionHandler
|
||||||
import services.DeleteObjectRequest
|
import services.DeleteObjectRequest
|
||||||
import services.DeleteObjectRequestJsonProtocol._
|
import services.DeleteObjectRequestJsonProtocol._
|
||||||
|
import services.JwtAuthenticator.{authenticated, hasPermissions}
|
||||||
import spray.json.DefaultJsonProtocol._
|
import spray.json.DefaultJsonProtocol._
|
||||||
import services.ImageSubmissionJsonProtocol._
|
import services.ImageSubmissionJsonProtocol._
|
||||||
|
|
||||||
@@ -21,11 +22,14 @@ import java.net.URLEncoder
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import scala.concurrent.ExecutionContextExecutor
|
import scala.concurrent.ExecutionContextExecutor
|
||||||
import scala.io.StdIn
|
import scala.io.StdIn
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
object ShotgunServer {
|
object ShotgunServer {
|
||||||
|
|
||||||
def main(args: Array[String]): Unit = {
|
def main(args: Array[String]): Unit = {
|
||||||
|
|
||||||
|
val logger = LoggerFactory.getLogger(getClass)
|
||||||
|
|
||||||
implicit val system: ActorSystem = ActorSystem("my-system")
|
implicit val system: ActorSystem = ActorSystem("my-system")
|
||||||
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
|
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
|
||||||
val logging = Logging(system, getClass)
|
val logging = Logging(system, getClass)
|
||||||
@@ -82,35 +86,34 @@ object ShotgunServer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
path("presigned-urls") {
|
path("presigned-urls") {
|
||||||
get {
|
hasPermissions(List("photo:upload")) { claim =>
|
||||||
parameters("count".as[Int], "contentType".as[String], "author".as[String]) { (imageCount, contentType, author) =>
|
get {
|
||||||
if (imageCount > 5)
|
parameters("count".as[Int], "contentType".as[String], "author".as[String]) { (imageCount, contentType, author) =>
|
||||||
complete(StatusCodes.BadRequest, "Cannot request more than 5 presigned URLs at a time")
|
if (imageCount > 5)
|
||||||
else {
|
complete(StatusCodes.BadRequest, "Cannot request more than 5 presigned URLs at a time")
|
||||||
val urls = awsClient.getMultiplePutPresignedUrls("deflock-photo-uploads", imageCount, contentType, author, 5)
|
else {
|
||||||
complete(urls)
|
val urls = awsClient.getMultiplePutPresignedUrls("deflock-photo-uploads", imageCount, contentType, author, 5)
|
||||||
|
complete(urls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
path("user-submissions") {
|
path("user-submissions") {
|
||||||
get {
|
hasPermissions(List("photo:view")) { claim =>
|
||||||
val submissions = awsClient.getAllObjects("deflock-photo-uploads")
|
get {
|
||||||
complete(submissions)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
path("oauth2" / "callback") {
|
path("delete-object") {
|
||||||
get {
|
hasPermissions(List("photo:delete")) { claim =>
|
||||||
parameters(Symbol("code").?) { (code) =>
|
post {
|
||||||
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to Pekko HTTP</h1><p><b>Code: " + code.getOrElse("None") + "</b></p>"))
|
entity(as[DeleteObjectRequest]) { request =>
|
||||||
|
val res = awsClient.deleteObject("deflock-photo-uploads", request.objectKey)
|
||||||
|
complete(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ const auth0 = createAuth0({
|
|||||||
clientId: "IEBa7ckgWrMGErTWXA8Z9q91hre7uII2",
|
clientId: "IEBa7ckgWrMGErTWXA8Z9q91hre7uII2",
|
||||||
authorizationParams: {
|
authorizationParams: {
|
||||||
redirect_uri: window.location.origin,
|
redirect_uri: window.location.origin,
|
||||||
|
audience: "https://deflock.me/api",
|
||||||
},
|
},
|
||||||
cacheLocation: 'localstorage',
|
cacheLocation: 'localstorage',
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user