Support JWT
This commit is contained in:
parent
25318cd6de
commit
aa9d60e4d1
9 changed files with 96 additions and 19 deletions
|
|
@ -16,6 +16,8 @@ import zio.logging.LogFormat.*
|
|||
import zio.logging.consoleLogger
|
||||
import zio.schema.codec.JsonCodec.ExplicitConfig
|
||||
|
||||
import lu.foyer.auth.AuthMiddleware.jwtAuthentication
|
||||
import lu.foyer.auth.JwtScalaTokenService
|
||||
import lu.foyer.clients.*
|
||||
import lu.foyer.contracts.*
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ object HttpServer:
|
|||
contract <- ZIO.service[ContractController]
|
||||
openAPI = OpenAPIGen.fromEndpoints(client.endpoints ++ contract.endpoints)
|
||||
yield (client.routes ++ contract.routes)
|
||||
@@ jwtAuthentication("Insurance")
|
||||
@@ cors(corsConfig) @@ Middleware.debug
|
||||
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||
|
||||
|
|
@ -52,10 +55,12 @@ object App extends ZIOAppDefault:
|
|||
val app =
|
||||
for
|
||||
routes <- HttpServer.routes
|
||||
server <- Server.serve(routes).provide(Server.default)
|
||||
server <- Server.serve(routes)
|
||||
yield server
|
||||
|
||||
override def run = app.provide(
|
||||
Server.default,
|
||||
JwtScalaTokenService.live,
|
||||
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
||||
CommandEngine.layer[ContractCommand, ContractEvent, ContractState],
|
||||
ClientHandlers.layer,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import zio.http.codec.PathCodec.path
|
|||
import zio.http.endpoint.*
|
||||
import zio.schema.*
|
||||
|
||||
import lu.foyer.auth.UserInfo
|
||||
|
||||
trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
|
||||
|
||||
def commandEngine: CommandEngine[Command, Event, State]
|
||||
|
|
@ -36,12 +38,12 @@ trait CommandEngineController[Command, Event: Schema, State: Schema] extends Jso
|
|||
|
||||
private def generateCreateCommand(handler: CommandHandler[Command, Event, State]) =
|
||||
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
||||
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
||||
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
||||
.in[Command]
|
||||
.jsonApiOneWithStatus[Event](Status.Created)
|
||||
val route = endpoint.implementJsonApiOneEvent(command =>
|
||||
for
|
||||
entityId <- Random.nextUUID
|
||||
entityId <- Random.nextUUID
|
||||
(event, _) <- commandEngine
|
||||
.handleCommand(command, handler.name, entityId.toString)
|
||||
yield Some(event)
|
||||
|
|
@ -50,7 +52,7 @@ trait CommandEngineController[Command, Event: Schema, State: Schema] extends Jso
|
|||
|
||||
private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) =
|
||||
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
||||
val endpoint = Endpoint(
|
||||
val endpoint = Endpoint(
|
||||
Method.PUT / entityName / string("entityId") / "commands" / handler.name
|
||||
)
|
||||
.in[Command]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import zio.schema.*
|
|||
|
||||
import lu.foyer.JsonApiResponse.Many
|
||||
import lu.foyer.JsonApiResponse.One
|
||||
import lu.foyer.auth.UserInfo
|
||||
|
||||
object JsonApiResponse:
|
||||
|
||||
|
|
@ -89,7 +90,7 @@ trait JsonApiController:
|
|||
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
||||
)
|
||||
def implementJsonApiOne[Env, A](
|
||||
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
|
||||
f: Input => ZIO[Env & UserInfo, JsonApiError | Throwable, Option[A]],
|
||||
getId: A => String,
|
||||
getEntity: A => Output,
|
||||
onthology: String = this.onthology,
|
||||
|
|
@ -99,7 +100,7 @@ trait JsonApiController:
|
|||
_: ProxyHeaders
|
||||
) => None
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
endpoint.implement(input =>
|
||||
for
|
||||
item <- f(input)
|
||||
|
|
@ -127,7 +128,7 @@ trait JsonApiController:
|
|||
def implementJsonApiOneEntity[Env](
|
||||
f: Input => RIO[Env, Option[Entity[Output]]]
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
implementJsonApiOne(
|
||||
f,
|
||||
_.entityId,
|
||||
|
|
@ -140,9 +141,9 @@ trait JsonApiController:
|
|||
)
|
||||
|
||||
def implementJsonApiOneEvent[Env](
|
||||
f: Input => ZIO[Env, JsonApiError | Throwable, Option[Event[Output]]]
|
||||
f: Input => ZIO[Env & UserInfo, JsonApiError | Throwable, Option[Event[Output]]]
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
implementJsonApiOne(
|
||||
f,
|
||||
_.eventId,
|
||||
|
|
|
|||
25
api/src/lu/foyer/auth/AuthMiddleware.scala
Normal file
25
api/src/lu/foyer/auth/AuthMiddleware.scala
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
import zio.*
|
||||
import zio.http.*
|
||||
|
||||
object AuthMiddleware:
|
||||
def jwtAuthentication(realm: String): HandlerAspect[JwtTokenService, UserInfo] =
|
||||
HandlerAspect.interceptIncomingHandler {
|
||||
handler { (request: Request) =>
|
||||
request.header(Header.Authorization) match
|
||||
case Some(Header.Authorization.Bearer(token)) =>
|
||||
ZIO
|
||||
.serviceWithZIO[JwtTokenService](_.verify(token.value.asString))
|
||||
.map(UserInfo(_))
|
||||
.map(userInfo => (request, userInfo))
|
||||
.orElseFail(
|
||||
Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm)))
|
||||
)
|
||||
case _ =>
|
||||
ZIO.fail(
|
||||
Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm)))
|
||||
)
|
||||
}
|
||||
}
|
||||
29
api/src/lu/foyer/auth/JwtScalaTokenService.scala
Normal file
29
api/src/lu/foyer/auth/JwtScalaTokenService.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
import java.time.Clock
|
||||
import java.util.Base64
|
||||
|
||||
import zio.*
|
||||
import zio.json.*
|
||||
|
||||
import lu.foyer.auth.JwtScalaTokenService.Claims
|
||||
|
||||
case class JwtScalaTokenService() extends JwtTokenService:
|
||||
implicit val clock: Clock = Clock.systemUTC
|
||||
|
||||
// Doesn't actually verify, we just pretend for simplicity
|
||||
override def verify(token: String): Task[String] =
|
||||
token.split('.').drop(1).headOption match
|
||||
case Some(base64) =>
|
||||
new String(Base64.getDecoder().decode(base64)).fromJson[Claims] match
|
||||
case Right(claims) => ZIO.succeed(claims.sub)
|
||||
case Left(error) =>
|
||||
ZIO.logWarning(s"Failed to parse JWT claims : $error") *>
|
||||
ZIO.fail(new Exception("Invalid token"))
|
||||
case None => ZIO.fail(new Exception("Invalid token"))
|
||||
|
||||
object JwtScalaTokenService:
|
||||
val live = ZLayer.succeed(JwtScalaTokenService())
|
||||
|
||||
final private[auth] case class Claims(sub: String) derives JsonDecoder
|
||||
Loading…
Add table
Add a link
Reference in a new issue