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.logging.consoleLogger
|
||||||
import zio.schema.codec.JsonCodec.ExplicitConfig
|
import zio.schema.codec.JsonCodec.ExplicitConfig
|
||||||
|
|
||||||
|
import lu.foyer.auth.AuthMiddleware.jwtAuthentication
|
||||||
|
import lu.foyer.auth.JwtScalaTokenService
|
||||||
import lu.foyer.clients.*
|
import lu.foyer.clients.*
|
||||||
import lu.foyer.contracts.*
|
import lu.foyer.contracts.*
|
||||||
|
|
||||||
|
|
@ -29,6 +31,7 @@ object HttpServer:
|
||||||
contract <- ZIO.service[ContractController]
|
contract <- ZIO.service[ContractController]
|
||||||
openAPI = OpenAPIGen.fromEndpoints(client.endpoints ++ contract.endpoints)
|
openAPI = OpenAPIGen.fromEndpoints(client.endpoints ++ contract.endpoints)
|
||||||
yield (client.routes ++ contract.routes)
|
yield (client.routes ++ contract.routes)
|
||||||
|
@@ jwtAuthentication("Insurance")
|
||||||
@@ cors(corsConfig) @@ Middleware.debug
|
@@ cors(corsConfig) @@ Middleware.debug
|
||||||
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||||
|
|
||||||
|
|
@ -52,10 +55,12 @@ object App extends ZIOAppDefault:
|
||||||
val app =
|
val app =
|
||||||
for
|
for
|
||||||
routes <- HttpServer.routes
|
routes <- HttpServer.routes
|
||||||
server <- Server.serve(routes).provide(Server.default)
|
server <- Server.serve(routes)
|
||||||
yield server
|
yield server
|
||||||
|
|
||||||
override def run = app.provide(
|
override def run = app.provide(
|
||||||
|
Server.default,
|
||||||
|
JwtScalaTokenService.live,
|
||||||
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
||||||
CommandEngine.layer[ContractCommand, ContractEvent, ContractState],
|
CommandEngine.layer[ContractCommand, ContractEvent, ContractState],
|
||||||
ClientHandlers.layer,
|
ClientHandlers.layer,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import zio.http.codec.PathCodec.path
|
||||||
import zio.http.endpoint.*
|
import zio.http.endpoint.*
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
|
||||||
|
import lu.foyer.auth.UserInfo
|
||||||
|
|
||||||
trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
|
trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
|
||||||
|
|
||||||
def commandEngine: CommandEngine[Command, Event, State]
|
def commandEngine: CommandEngine[Command, Event, State]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import zio.schema.*
|
||||||
|
|
||||||
import lu.foyer.JsonApiResponse.Many
|
import lu.foyer.JsonApiResponse.Many
|
||||||
import lu.foyer.JsonApiResponse.One
|
import lu.foyer.JsonApiResponse.One
|
||||||
|
import lu.foyer.auth.UserInfo
|
||||||
|
|
||||||
object JsonApiResponse:
|
object JsonApiResponse:
|
||||||
|
|
||||||
|
|
@ -89,7 +90,7 @@ trait JsonApiController:
|
||||||
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
||||||
)
|
)
|
||||||
def implementJsonApiOne[Env, A](
|
def implementJsonApiOne[Env, A](
|
||||||
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
|
f: Input => ZIO[Env & UserInfo, JsonApiError | Throwable, Option[A]],
|
||||||
getId: A => String,
|
getId: A => String,
|
||||||
getEntity: A => Output,
|
getEntity: A => Output,
|
||||||
onthology: String = this.onthology,
|
onthology: String = this.onthology,
|
||||||
|
|
@ -99,7 +100,7 @@ trait JsonApiController:
|
||||||
_: ProxyHeaders
|
_: ProxyHeaders
|
||||||
) => None
|
) => None
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env & ProxyHeaders, Nothing] =
|
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||||
endpoint.implement(input =>
|
endpoint.implement(input =>
|
||||||
for
|
for
|
||||||
item <- f(input)
|
item <- f(input)
|
||||||
|
|
@ -127,7 +128,7 @@ trait JsonApiController:
|
||||||
def implementJsonApiOneEntity[Env](
|
def implementJsonApiOneEntity[Env](
|
||||||
f: Input => RIO[Env, Option[Entity[Output]]]
|
f: Input => RIO[Env, Option[Entity[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env & ProxyHeaders, Nothing] =
|
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||||
implementJsonApiOne(
|
implementJsonApiOne(
|
||||||
f,
|
f,
|
||||||
_.entityId,
|
_.entityId,
|
||||||
|
|
@ -140,9 +141,9 @@ trait JsonApiController:
|
||||||
)
|
)
|
||||||
|
|
||||||
def implementJsonApiOneEvent[Env](
|
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
|
)(implicit trace: Trace
|
||||||
): Route[Env & ProxyHeaders, Nothing] =
|
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||||
implementJsonApiOne(
|
implementJsonApiOne(
|
||||||
f,
|
f,
|
||||||
_.eventId,
|
_.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
|
||||||
|
|
@ -3,6 +3,8 @@ package lu.foyer
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.schema.Schema
|
import zio.schema.Schema
|
||||||
|
|
||||||
|
import lu.foyer.auth.UserInfo
|
||||||
|
|
||||||
final case class Entity[T](entityId: String, data: T, version: Long)
|
final case class Entity[T](entityId: String, data: T, version: Long)
|
||||||
final case class Event[T](entityId: String, data: T, eventId: String)
|
final case class Event[T](entityId: String, data: T, eventId: String)
|
||||||
|
|
||||||
|
|
@ -28,13 +30,14 @@ trait CommandHandler[+Command, +Event, +State]:
|
||||||
def commandSchema: Schema[?]
|
def commandSchema: Schema[?]
|
||||||
|
|
||||||
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
|
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
|
||||||
def onCommand(entityId: String, command: Command): IO[JsonApiError | Throwable, Event]
|
def onCommand(entityId: String, command: Command): ZIO[UserInfo, JsonApiError | Throwable, Event]
|
||||||
val isCreate = true
|
val isCreate = true
|
||||||
val commandSchema = summon[Schema[Command]]
|
val commandSchema = summon[Schema[Command]]
|
||||||
|
|
||||||
trait CommandHandlerUpdate[Command: Schema, Event, State]
|
trait CommandHandlerUpdate[Command: Schema, Event, State]
|
||||||
extends CommandHandler[Command, Event, State]:
|
extends CommandHandler[Command, Event, State]:
|
||||||
def onCommand(entityId: String, state: State, command: Command) : IO[JsonApiError | Throwable, Event]
|
def onCommand(entityId: String, state: State, command: Command)
|
||||||
|
: ZIO[UserInfo, JsonApiError | Throwable, Event]
|
||||||
val isCreate = false
|
val isCreate = false
|
||||||
val commandSchema = summon[Schema[Command]]
|
val commandSchema = summon[Schema[Command]]
|
||||||
|
|
||||||
|
|
@ -45,7 +48,7 @@ class CommandEngine[Command, Event, State](
|
||||||
val stateRepo: StateRepository[State]):
|
val stateRepo: StateRepository[State]):
|
||||||
|
|
||||||
def handleCommand(command: Command, name: String, entityId: String)
|
def handleCommand(command: Command, name: String, entityId: String)
|
||||||
: IO[JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
: ZIO[UserInfo, JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
||||||
for
|
for
|
||||||
handler <- ZIO
|
handler <- ZIO
|
||||||
.succeed(handlers.find(_.name == name))
|
.succeed(handlers.find(_.name == name))
|
||||||
|
|
@ -70,7 +73,7 @@ class CommandEngine[Command, Event, State](
|
||||||
entityId: String,
|
entityId: String,
|
||||||
entityOption: Option[Entity[State]],
|
entityOption: Option[Entity[State]],
|
||||||
handler: CommandHandler[Command, Event, State]
|
handler: CommandHandler[Command, Event, State]
|
||||||
): IO[JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
|
): ZIO[UserInfo, JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
|
||||||
case (None, h) if !h.isUpdate =>
|
case (None, h) if !h.isUpdate =>
|
||||||
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
|
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
|
||||||
.onCommand(entityId, command)
|
.onCommand(entityId, command)
|
||||||
|
|
|
||||||
7
core/src/lu/foyer/auth/JwtTokenService.scala
Normal file
7
core/src/lu/foyer/auth/JwtTokenService.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package lu.foyer
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import zio.*
|
||||||
|
|
||||||
|
trait JwtTokenService:
|
||||||
|
def verify(token: String): Task[String]
|
||||||
4
core/src/lu/foyer/auth/UserInfo.scala
Normal file
4
core/src/lu/foyer/auth/UserInfo.scala
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
package lu.foyer
|
||||||
|
package auth
|
||||||
|
|
||||||
|
final case class UserInfo(subject: String)
|
||||||
|
|
@ -3,6 +3,7 @@ package contracts
|
||||||
|
|
||||||
import zio.*
|
import zio.*
|
||||||
|
|
||||||
|
import lu.foyer.auth.UserInfo
|
||||||
import lu.foyer.clients.*
|
import lu.foyer.clients.*
|
||||||
|
|
||||||
object ContractHandlers:
|
object ContractHandlers:
|
||||||
|
|
@ -92,11 +93,11 @@ class ApproveHandler(employeeService: EmployeeService)
|
||||||
val name = "approve"
|
val name = "approve"
|
||||||
|
|
||||||
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
|
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
|
||||||
: Task[ContractEvent.Approved] =
|
: RIO[UserInfo, ContractEvent.Approved] =
|
||||||
for
|
for
|
||||||
user <- ZIO.succeed("") // TODO current user
|
user <- ZIO.service[UserInfo]
|
||||||
employee <- ZIO
|
employee <- ZIO
|
||||||
.fromEither(employeeService.fetchOne(user))
|
.fromEither(employeeService.fetchOne(user.subject))
|
||||||
.mapError(new IllegalArgumentException(_))
|
.mapError(new IllegalArgumentException(_))
|
||||||
yield ContractEvent.Approved(employee)
|
yield ContractEvent.Approved(employee)
|
||||||
|
|
||||||
|
|
@ -110,11 +111,11 @@ class RejectHandler(employeeService: EmployeeService)
|
||||||
val name = "reject"
|
val name = "reject"
|
||||||
|
|
||||||
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
|
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
|
||||||
: Task[ContractEvent.Rejected] =
|
: RIO[UserInfo, ContractEvent.Rejected] =
|
||||||
for
|
for
|
||||||
user <- ZIO.succeed("") // TODO current user
|
user <- ZIO.service[UserInfo]
|
||||||
employee <- ZIO
|
employee <- ZIO
|
||||||
.fromEither(employeeService.fetchOne(user))
|
.fromEither(employeeService.fetchOne(user.subject))
|
||||||
.mapError(new IllegalArgumentException(_))
|
.mapError(new IllegalArgumentException(_))
|
||||||
yield ContractEvent.Rejected(employee, command.comment)
|
yield ContractEvent.Rejected(employee, command.comment)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue