Refactor: Move bcrypt inside kodein module for easier testing

This commit is contained in:
Hubert Van De Walle 2020-06-18 16:50:39 +02:00
parent 214286a6eb
commit 1611ca6ab4
8 changed files with 39 additions and 20 deletions

View File

@ -1,5 +1,7 @@
package be.vandewalleh package be.vandewalleh
import be.vandewalleh.features.BcryptPasswordHash
import be.vandewalleh.features.PasswordHash
import be.vandewalleh.features.configurationModule import be.vandewalleh.features.configurationModule
import be.vandewalleh.migrations.Migration import be.vandewalleh.migrations.Migration
import be.vandewalleh.services.serviceModule import be.vandewalleh.services.serviceModule
@ -18,4 +20,5 @@ val mainModule = Kodein.Module("main") {
bind<Logger>() with singleton { LoggerFactory.getLogger("Application") } bind<Logger>() with singleton { LoggerFactory.getLogger("Application") }
bind<Migration>() with singleton { Migration(this.kodein) } bind<Migration>() with singleton { Migration(this.kodein) }
bind<Database>() with singleton { Database.connect(this.instance<DataSource>()) } bind<Database>() with singleton { Database.connect(this.instance<DataSource>()) }
bind<PasswordHash>() with singleton { BcryptPasswordHash() }
} }

View File

@ -1,12 +1,15 @@
package be.vandewalleh.entities package be.vandewalleh.entities
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.*
interface User : Entity<User> { interface User : Entity<User> {
companion object : Entity.Factory<User>() companion object : Entity.Factory<User>()
@get:JsonIgnore
val id: Int val id: Int
var username: String var username: String
@get:JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @get:JsonProperty(access = JsonProperty.Access.WRITE_ONLY)

View File

@ -0,0 +1,13 @@
package be.vandewalleh.features
import org.mindrot.jbcrypt.BCrypt
interface PasswordHash {
fun crypt(password: String): String
fun verify(password: String, hashedPassword: String): Boolean
}
class BcryptPasswordHash : PasswordHash {
override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt())!!
override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword)
}

View File

@ -4,6 +4,7 @@ import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserDbIdPrincipal import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.auth.UsernamePasswordCredential import be.vandewalleh.auth.UsernamePasswordCredential
import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.features.PasswordHash
import be.vandewalleh.services.UserService import be.vandewalleh.services.UserService
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
import io.ktor.application.* import io.ktor.application.*
@ -14,7 +15,6 @@ import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
data class RefreshToken(val refreshToken: String) data class RefreshToken(val refreshToken: String)
data class DualToken(val token: String, val refreshToken: String) data class DualToken(val token: String, val refreshToken: String)
@ -23,6 +23,7 @@ fun Routing.auth(kodein: Kodein) {
val authSimpleJwt by kodein.instance<SimpleJWT>("auth") val authSimpleJwt by kodein.instance<SimpleJWT>("auth")
val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh") val refreshSimpleJwt by kodein.instance<SimpleJWT>("refresh")
val userService by kodein.instance<UserService>() val userService by kodein.instance<UserService>()
val passwordHash by kodein.instance<PasswordHash>()
post("/user/login") { post("/user/login") {
val credential = call.receive<UsernamePasswordCredential>() val credential = call.receive<UsernamePasswordCredential>()
@ -30,7 +31,7 @@ fun Routing.auth(kodein: Kodein) {
val user = userService.find(credential.username) val user = userService.find(credential.username)
?: return@post call.respondStatus(HttpStatusCode.Unauthorized) ?: return@post call.respondStatus(HttpStatusCode.Unauthorized)
if (!BCrypt.checkpw(credential.password, user.password)) { if (!passwordHash.verify(credential.password, user.password)) {
return@post call.respondStatus(HttpStatusCode.Unauthorized) return@post call.respondStatus(HttpStatusCode.Unauthorized)
} }

View File

@ -12,7 +12,6 @@ import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
fun Routing.user(kodein: Kodein) { fun Routing.user(kodein: Kodein) {
val userService by kodein.instance<UserService>() val userService by kodein.instance<UserService>()
@ -24,9 +23,7 @@ fun Routing.user(kodein: Kodein) {
if (userService.exists(user.username)) if (userService.exists(user.username))
return@post call.respondStatus(HttpStatusCode.Conflict) return@post call.respondStatus(HttpStatusCode.Conflict)
val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt()) val newUser = userService.create(user.username, user.password)
val newUser = userService.create(user.username, hashedPassword)
?: return@post call.respondStatus(HttpStatusCode.Conflict) ?: return@post call.respondStatus(HttpStatusCode.Conflict)
call.respond(HttpStatusCode.Created, newUser) call.respond(HttpStatusCode.Created, newUser)

View File

@ -1,10 +1,9 @@
package be.vandewalleh.services package be.vandewalleh.services
import be.vandewalleh.entities.User import be.vandewalleh.entities.User
import be.vandewalleh.extensions.ioAsync
import be.vandewalleh.extensions.launchIo import be.vandewalleh.extensions.launchIo
import be.vandewalleh.features.PasswordHash
import be.vandewalleh.tables.Users import be.vandewalleh.tables.Users
import kotlinx.coroutines.Deferred
import me.liuwj.ktorm.database.* import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.*
@ -12,13 +11,13 @@ import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
import java.time.LocalDateTime
/** /**
* service to handle database queries for users. * service to handle database queries for users.
*/ */
class UserService(override val kodein: Kodein) : KodeinAware { class UserService(override val kodein: Kodein) : KodeinAware {
private val db by instance<Database>() private val db by instance<Database>()
private val passwordHash by instance<PasswordHash>()
/** /**
@ -49,10 +48,10 @@ class UserService(override val kodein: Kodein) : KodeinAware {
* create a new user * create a new user
* password should already be hashed * password should already be hashed
*/ */
suspend fun create(username: String, hashedPassword: String): User? { suspend fun create(username: String, password: String): User? {
val newUser = User { val newUser = User {
this.username = username this.username = username
password = hashedPassword this.password = passwordHash.crypt(password)
} }
return try { return try {

View File

@ -3,6 +3,7 @@ package integration.routing
import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.entities.User import be.vandewalleh.entities.User
import be.vandewalleh.features.Config import be.vandewalleh.features.Config
import be.vandewalleh.features.PasswordHash
import be.vandewalleh.mainModule import be.vandewalleh.mainModule
import be.vandewalleh.module import be.vandewalleh.module
import be.vandewalleh.services.UserService import be.vandewalleh.services.UserService
@ -19,7 +20,6 @@ import org.junit.jupiter.api.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.bind import org.kodein.di.generic.bind
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt
import utils.* import utils.*
import java.util.* import java.util.*
@ -28,10 +28,18 @@ class AuthControllerKtTest {
private val userService = mockk<UserService>() private val userService = mockk<UserService>()
private val kodein = Kodein {
import(mainModule, allowOverride = true)
bind<UserService>(overrides = true) with instance(userService)
}
private val passwordHash by kodein.instance<PasswordHash>()
init { init {
val user = User { val user = User {
password = BCrypt.hashpw("password", BCrypt.gensalt()) password = passwordHash.crypt("password")
username = "existing" username = "existing"
} }
user["id"] = 1 user["id"] = 1
@ -43,7 +51,7 @@ class AuthControllerKtTest {
} }
val user2 = User { val user2 = User {
password = BCrypt.hashpw("right password", BCrypt.gensalt()) password = passwordHash.crypt("right password")
username = "wrong" username = "wrong"
} }
user["id"] = 2 user["id"] = 2
@ -56,11 +64,6 @@ class AuthControllerKtTest {
} }
private val kodein = Kodein {
import(mainModule, allowOverride = true)
bind<UserService>(overrides = true) with instance(userService)
}
private val testEngine = TestApplicationEngine().apply { private val testEngine = TestApplicationEngine().apply {
start() start()
application.module(kodein) application.module(kodein)

View File

@ -52,7 +52,7 @@ class UserServiceTest {
@Order(2) @Order(2)
fun `test create same user`() { fun `test create same user`() {
runBlocking { runBlocking {
userService.create(username = "hubert", hashedPassword = "password") `should be` null userService.create(username = "hubert", password = "password") `should be` null
} }
} }