Add JwtFilter + separate modules

This commit is contained in:
Hubert Van De Walle 2020-09-29 19:25:39 +02:00
parent a50bd89e18
commit b8e6c4b7ca
8 changed files with 133 additions and 83 deletions

View File

@ -1,76 +1,13 @@
package be.simplenotes package be.simplenotes
import be.simplenotes.extensions.addShutdownHook import be.simplenotes.extensions.addShutdownHook
import be.simplenotes.extensions.hikariDataSource import be.simplenotes.modules.httpModule
import be.simplenotes.extensions.registerSerializers import be.simplenotes.modules.jacksonModule
import be.simplenotes.repositories.UserRepository import be.simplenotes.modules.persistanceModule
import be.simplenotes.routes.RouteSupplier import be.simplenotes.modules.securityModule
import be.simplenotes.routes.UserRoutes
import be.simplenotes.routes.toRouter
import be.simplenotes.security.Argon2PasswordHash
import be.simplenotes.security.PasswordHash
import be.simplenotes.security.SimpleJwt
import be.simplenotes.serializers.UserIdSerializer
import be.simplenotes.serializers.UserSerializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import org.flywaydb.core.Flyway
import org.http4k.core.then
import org.http4k.filter.ServerFilters
import org.http4k.format.Jackson
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ApacheServer
import org.http4k.server.asServer
import org.jooq.SQLDialect
import org.jooq.impl.DSL
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.dsl.onClose
import java.util.concurrent.TimeUnit
import javax.sql.DataSource
fun main() { fun main() = startKoin {
val jacksonModule = module {
single { UserIdSerializer() } bind StdSerializer::class
single { UserSerializer() } bind StdSerializer::class
single { Jackson.mapper }
single(createdAtStart = true) {
get<ObjectMapper>().registerSerializers(getAll())
}
}
val persistanceModule = module {
single { UserRepository(get(), get()) }
single {
hikariDataSource {
jdbcUrl = "jdbc:h2:./notes.db;MODE=MySQL;"
driverClassName = "org.h2.Driver"
maximumPoolSize = 2
}
} bind DataSource::class onClose { it?.close() }
single { Flyway.configure().dataSource(get()).load() }
single { get<Flyway>().migrate(); DSL.using(get<DataSource>(), SQLDialect.H2) }
}
val securityModule = module {
single { SimpleJwt("super secret", 1, TimeUnit.HOURS) }
single<PasswordHash> { Argon2PasswordHash() }
}
val httpModule = module {
single { UserRoutes(get(), get()) } bind RouteSupplier::class
single { getAll<RouteSupplier>().toRouter() }
single(createdAtStart = true) {
ServerFilters.CatchLensFailure()
.then(get<RoutingHttpHandler>())
.asServer(ApacheServer(7000)).start()
} onClose { it?.close() }
}
startKoin {
modules(httpModule, securityModule, jacksonModule, persistanceModule) modules(httpModule, securityModule, jacksonModule, persistanceModule)
}.koin.addShutdownHook() }.addShutdownHook()
}

View File

@ -1,9 +1,9 @@
package be.simplenotes.extensions package be.simplenotes.extensions
import org.koin.core.Koin import org.koin.core.KoinApplication
import kotlin.concurrent.thread import kotlin.concurrent.thread
fun Koin.addShutdownHook() { fun KoinApplication.addShutdownHook() {
Runtime.getRuntime().addShutdownHook( Runtime.getRuntime().addShutdownHook(
thread(start = false) { thread(start = false) {
close() close()

View File

@ -0,0 +1,29 @@
package be.simplenotes.filters
import be.simplenotes.security.SimpleJwt
import org.http4k.core.Filter
import org.http4k.core.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.cookie.cookie
class JwtFilter(
private val ctx: RequestContexts,
private val simpleJwt: SimpleJwt,
) {
private val authKey = "auth"
operator fun invoke() = Filter { next ->
{ req ->
val extractedToken = req.cookie("Bearer")
?.value
?.trim()
?.let { simpleJwt.extract(it) }
if (extractedToken != null) {
ctx[req][authKey] = extractedToken
next(req)
} else Response(Status.UNAUTHORIZED)
}
}
}

View File

@ -0,0 +1,28 @@
package be.simplenotes.modules
import be.simplenotes.filters.JwtFilter
import be.simplenotes.routes.RouteSupplier
import be.simplenotes.routes.UserRoutes
import be.simplenotes.routes.toRouter
import org.http4k.core.RequestContexts
import org.http4k.core.then
import org.http4k.filter.ServerFilters
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ApacheServer
import org.http4k.server.asServer
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.dsl.onClose
val httpModule = module {
single { UserRoutes(get(), get(), get(), get()) } bind RouteSupplier::class
single { getAll<RouteSupplier>().toRouter() }
single { RequestContexts() }
single { JwtFilter(get(), get())() }
single(createdAtStart = true) {
ServerFilters.CatchLensFailure()
.then(ServerFilters.InitialiseRequestContext(get<RequestContexts>()))
.then(get<RoutingHttpHandler>())
.asServer(ApacheServer(7000)).start()
} onClose { it?.close() }
}

View File

@ -0,0 +1,17 @@
package be.simplenotes.modules
import be.simplenotes.extensions.registerSerializers
import be.simplenotes.serializers.UserIdSerializer
import be.simplenotes.serializers.UserSerializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import org.http4k.format.Jackson
import org.koin.dsl.bind
import org.koin.dsl.module
val jacksonModule = module {
single { UserIdSerializer() } bind StdSerializer::class
single { UserSerializer() } bind StdSerializer::class
single { Jackson.mapper }
single(createdAtStart = true) { get<ObjectMapper>().registerSerializers(getAll()) }
}

View File

@ -0,0 +1,26 @@
package be.simplenotes.modules
import be.simplenotes.extensions.hikariDataSource
import be.simplenotes.repositories.UserRepository
import org.flywaydb.core.Flyway
import org.jooq.SQLDialect
import org.jooq.impl.DSL
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.dsl.onClose
import javax.sql.DataSource
val persistanceModule = module {
single { UserRepository(get(), get()) }
single {
hikariDataSource {
jdbcUrl = "jdbc:h2:./notes.db;MODE=MySQL;"
driverClassName = "org.h2.Driver"
maximumPoolSize = 2
}
} bind DataSource::class onClose { it?.close() }
single { Flyway.configure().dataSource(get()).load() }
single { get<Flyway>().migrate(); DSL.using(get<DataSource>(), SQLDialect.H2) }
}

View File

@ -0,0 +1,12 @@
package be.simplenotes.modules
import be.simplenotes.security.Argon2PasswordHash
import be.simplenotes.security.PasswordHash
import be.simplenotes.security.SimpleJwt
import org.koin.dsl.module
import java.util.concurrent.TimeUnit
val securityModule = module {
single { SimpleJwt("super secret", 1, TimeUnit.HOURS) }
single<PasswordHash> { Argon2PasswordHash() }
}

View File

@ -6,16 +6,14 @@ import be.simplenotes.extensions.notFound
import be.simplenotes.extensions.ok import be.simplenotes.extensions.ok
import be.simplenotes.repositories.UserRepository import be.simplenotes.repositories.UserRepository
import be.simplenotes.security.SimpleJwt import be.simplenotes.security.SimpleJwt
import org.http4k.core.Body import org.http4k.core.*
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.core.Response
import org.http4k.core.cookie.Cookie import org.http4k.core.cookie.Cookie
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.with
import org.http4k.format.Jackson.auto import org.http4k.format.Jackson.auto
import org.http4k.lens.ContentNegotiation import org.http4k.lens.ContentNegotiation
import org.http4k.lens.Cookies
import org.http4k.lens.Path import org.http4k.lens.Path
import org.http4k.lens.RequestContextKey
import org.http4k.lens.long import org.http4k.lens.long
import org.http4k.routing.bind import org.http4k.routing.bind
import org.http4k.routing.routes import org.http4k.routing.routes
@ -23,12 +21,17 @@ import org.http4k.routing.routes
data class Message(val msg: String) data class Message(val msg: String)
data class Login(val username: String, val password: String) data class Login(val username: String, val password: String)
class UserRoutes(private val userRepository: UserRepository, private val simpleJwt: SimpleJwt) : RouteSupplier { class UserRoutes(
private val userRepository: UserRepository,
private val simpleJwt: SimpleJwt,
private val jwtFilter: Filter,
private val ctx: RequestContexts,
) : RouteSupplier {
private val userLens = Body.auto<User>("user", ContentNegotiation.StrictNoDirective).toLens() private val userLens = Body.auto<User>("user", ContentNegotiation.StrictNoDirective).toLens()
private val msgLens = Body.auto<Message>().toLens() private val msgLens = Body.auto<Message>().toLens()
private val loginLens = Body.auto<Login>().toLens() private val loginLens = Body.auto<Login>().toLens()
private val idLens = Path.long().map { UserId(it) }.of("id") private val idLens = Path.long().map { UserId(it) }.of("id")
private val jwtLens = Cookies.optional("Bearer") private val userIdLens = RequestContextKey.required<UserId>(ctx, "auth")
private fun userNotFound() = Response.notFound().with(msgLens of Message("not found")) private fun userNotFound() = Response.notFound().with(msgLens of Message("not found"))
@ -57,11 +60,9 @@ class UserRoutes(private val userRepository: UserRepository, private val simpleJ
Response.ok().with(userLens of user.copy(id = null)).cookie(cookie) Response.ok().with(userLens of user.copy(id = null)).cookie(cookie)
} ?: Response.notFound() } ?: Response.notFound()
}, },
"/whoami" bind GET to { "/private" bind GET to jwtFilter.then {
jwtLens(it)?.value val user = userRepository.find(userIdLens(it))!!
?.let { token -> simpleJwt.extract(token) } Response.ok().with(msgLens of Message("Welcome $user"))
?.let { id -> Response.ok().with(userLens of userRepository.find(id)!!) }
?: Response.notFound()
} }
) )
} }