mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 23:12:48 +00: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 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,
|
||||
|
||||
14
shotgun/src/main/resources/logback.xml
Normal file
14
shotgun/src/main/resources/logback.xml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
shotgun/src/main/scala/services/JwtAuthenticator.scala
Normal file
88
shotgun/src/main/scala/services/JwtAuthenticator.scala
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const auth0 = createAuth0({
|
||||
clientId: "IEBa7ckgWrMGErTWXA8Z9q91hre7uII2",
|
||||
authorizationParams: {
|
||||
redirect_uri: window.location.origin,
|
||||
audience: "https://deflock.me/api",
|
||||
},
|
||||
cacheLocation: 'localstorage',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user