From 745156ccdc527919486a62ceb4351b1fdcf62a35 Mon Sep 17 00:00:00 2001 From: Ruslan Ibragimov Date: Tue, 7 Jan 2025 23:40:28 +0300 Subject: [PATCH] Add token to session store, implement login handler --- .../komok/business/AnonymousRoutesModule.kt | 6 +- .../heapy/komok/business/login/LoginModule.kt | 33 ++++++++++ .../komok/business/login/LoginRequest.kt | 10 +++ .../komok/business/login/LoginResponse.kt | 10 +++ .../heapy/komok/business/login/LoginRoute.kt | 39 +++++++---- .../komok/business/login/LoginService.kt | 64 +++++++++++++++--- .../io/heapy/komok/business/user/UserDao.kt | 4 +- .../komok/business/user/UserDaoModule.kt | 15 +++++ .../business/user/session/UserSession.kt | 29 -------- .../business/user/session/UserSessionDao.kt | 66 +++++++++++++++++++ .../user/session/UserSessionDaoModule.kt | 18 +++++ .../user => infra}/argon2/Argon2id.kt | 15 ++--- .../argon2/PasswordHasherModule.kt | 2 +- .../heapy/komok/infra/http/server/Defaults.kt | 37 +++-------- .../server/errors/AuthenticationException.kt | 19 ++++++ .../server/errors/AuthorizationException.kt | 14 ++++ .../http/server/errors/BadRequestException.kt | 44 +++++++++++++ .../http/server/errors/BadRequestResponse.kt | 22 +++++++ .../login => infra/jwt}/JwtConfiguration.kt | 2 +- .../io/heapy/komok/infra/jwt/JwtModule.kt | 1 - .../io/heapy/komok/infra/jwt/JwtService.kt | 1 - .../session_token/SessionTokenService.kt} | 8 +-- .../SessionTokenServiceModule.kt | 17 +++++ .../totp/TimeBasedOneTimePasswordModule.kt | 4 +- ....kt => TimeBasedOneTimePasswordService.kt} | 6 +- .../argon2/Argon2idPasswordHasherTest.kt | 2 +- .../heapy/komok/infra/jwt/JwtServiceTest.kt | 1 - .../totp/TimeBasedOneTimePasswordTest.kt | 10 +-- .../kotlin/io/heapy/komok/dao/mg/MongoV1.kt | 3 +- 29 files changed, 389 insertions(+), 113 deletions(-) create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginModule.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRequest.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginResponse.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDaoModule.kt delete mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSession.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDao.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDaoModule.kt rename komok-app/src/main/kotlin/io/heapy/komok/{business/user => infra}/argon2/Argon2id.kt (91%) rename komok-app/src/main/kotlin/io/heapy/komok/{business/user => infra}/argon2/PasswordHasherModule.kt (92%) create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthenticationException.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthorizationException.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestException.kt create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestResponse.kt rename komok-app/src/main/kotlin/io/heapy/komok/{business/login => infra/jwt}/JwtConfiguration.kt (86%) rename komok-app/src/main/kotlin/io/heapy/komok/{business/user/session/SessionTokenGenerator.kt => infra/session_token/SessionTokenService.kt} (78%) create mode 100644 komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenServiceModule.kt rename komok-app/src/main/kotlin/io/heapy/komok/infra/totp/{TimeBasedOneTimePassword.kt => TimeBasedOneTimePasswordService.kt} (94%) rename komok-app/src/test/kotlin/io/heapy/komok/{business/user => infra}/argon2/Argon2idPasswordHasherTest.kt (95%) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/AnonymousRoutesModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/AnonymousRoutesModule.kt index f89cae3..2d1a984 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/AnonymousRoutesModule.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/AnonymousRoutesModule.kt @@ -1,16 +1,20 @@ package io.heapy.komok.business import io.heapy.komok.business.healthcheck.HealthCheckRoute +import io.heapy.komok.business.login.LoginModule import io.heapy.komok.server.common.KomokRoute import io.heapy.komok.server.common.KomokRoutes import io.heapy.komok.tech.di.lib.Module @Module -open class AnonymousRoutesModule { +open class AnonymousRoutesModule( + private val loginModule: LoginModule, +) { open val anonymousRoutes: KomokRoute by lazy { KomokRoutes( routes = listOf( healthCheckRoute, + loginModule.loginRoute, ), ) } diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginModule.kt new file mode 100644 index 0000000..3513052 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginModule.kt @@ -0,0 +1,33 @@ +package io.heapy.komok.business.login + +import io.heapy.komok.business.user.UserDaoModule +import io.heapy.komok.business.user.session.UserSessionDaoModule +import io.heapy.komok.infra.argon2.PasswordHasherModule +import io.heapy.komok.infra.session_token.SessionTokenServiceModule +import io.heapy.komok.infra.totp.TimeBasedOneTimePasswordModule +import io.heapy.komok.tech.di.lib.Module + +@Module +open class LoginModule( + private val userDaoModule: UserDaoModule, + private val userSessionDaoModule: UserSessionDaoModule, + private val sessionTokenServiceModule: SessionTokenServiceModule, + private val timeBasedOneTimePasswordModule: TimeBasedOneTimePasswordModule, + private val passwordHasherModule: PasswordHasherModule, +) { + open val loginService by lazy { + LoginService( + userDao = userDaoModule.userDao, + userSessionDao = userSessionDaoModule.userSessionDao, + sessionTokenService = sessionTokenServiceModule.sessionTokenService, + timeBasedOneTimePasswordService = timeBasedOneTimePasswordModule.timeBasedOneTimePasswordService, + passwordHasherModule.passwordHasher, + ) + } + + open val loginRoute by lazy { + LoginRoute( + loginService = loginService, + ) + } +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRequest.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRequest.kt new file mode 100644 index 0000000..da50a0a --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRequest.kt @@ -0,0 +1,10 @@ +package io.heapy.komok.business.login + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginRequest( + val email: String, + val password: String, + val otp: String, +) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginResponse.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginResponse.kt new file mode 100644 index 0000000..d78b1ab --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginResponse.kt @@ -0,0 +1,10 @@ +package io.heapy.komok.business.login + +import kotlinx.serialization.Serializable +import kotlin.time.Duration + +@Serializable +data class LoginResponse( + val sessionToken: String, + val maxAge: Duration, +) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRoute.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRoute.kt index 1f0b253..778183a 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRoute.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginRoute.kt @@ -1,25 +1,40 @@ package io.heapy.komok.business.login -import io.heapy.komok.infra.jwt.JwtService import io.heapy.komok.server.common.KomokRoute +import io.ktor.http.Cookie +import io.ktor.http.HttpStatusCode +import io.ktor.server.plugins.origin import io.ktor.server.request.receive +import io.ktor.server.response.respond import io.ktor.server.routing.* -import kotlinx.serialization.Serializable class LoginRoute( - private val jwtService: JwtService, + private val loginService: LoginService, ) : KomokRoute { - @Serializable - data class LoginRequest( - val email: String, - val password: String, - ) - override fun Routing.install() { post("/login") { - val req = call.receive() -// jwtService.createToken(User(id = "1")) -// call.respond(hashMapOf("token" to token)) + val loginRequest = call.receive() + val forwardedFor = call.request.headers["X-Forwarded-For"] + val clientIp = forwardedFor ?: call.request.origin.remoteHost + + val loginResponse = loginService + .login( + loginRequest = loginRequest, + ip = clientIp, + userAgent = call.request.headers["User-Agent"] ?: "Unknown", + ) + + call.response.cookies.append( + Cookie( + name = "JSESSIONID", + value = loginResponse.sessionToken, + maxAge = loginResponse.maxAge.inWholeSeconds.toInt(), + path = "/", + secure = true, + httpOnly = true, + ) + ) + call.respond(HttpStatusCode.OK) } } } diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginService.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginService.kt index 422a347..0686ffb 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginService.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/login/LoginService.kt @@ -1,24 +1,68 @@ package io.heapy.komok.business.login import io.heapy.komok.business.user.UserDao -import io.heapy.komok.business.user.session.SessionTokenGenerator +import io.heapy.komok.business.user.session.UserSessionDao +import io.heapy.komok.infra.argon2.PasswordHasher +import io.heapy.komok.infra.http.server.errors.badRequestError +import io.heapy.komok.infra.session_token.SessionTokenService +import io.heapy.komok.infra.totp.TimeBasedOneTimePasswordService +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds class LoginService( private val userDao: UserDao, - private val sessionTokenGenerator: SessionTokenGenerator + private val userSessionDao: UserSessionDao, + private val sessionTokenService: SessionTokenService, + private val timeBasedOneTimePasswordService: TimeBasedOneTimePasswordService, + private val passwordHasher: PasswordHasher, ) { suspend fun login( - email: String, - password: String, - totp: String, - ): String { - val user = userDao.getUser(email) - ?: throw IllegalArgumentException("User not found") + loginRequest: LoginRequest, + ip: String, + userAgent: String, + ): LoginResponse { + val user = userDao + .getUser( + email = loginRequest.email, + ) + ?: badRequestError("email", "User not found") + val passwordValid = passwordHasher.verify( + password = loginRequest.password, + hash = user.hash, + ) + if (!passwordValid) { + badRequestError("password", "Invalid password") + } - val sessionToken = sessionTokenGenerator.generate() + val isTotpValid = timeBasedOneTimePasswordService.validate( + secret = user.authenticatorKey, + totp = loginRequest.otp, + ) - return sessionToken + if (!isTotpValid) { + badRequestError("otp", "Invalid OTP") + } + + val sessionToken = sessionTokenService.generate() + + userSessionDao.createSession( + userId = user.id, + maxAge = sessionTokenMaxAge, + ip = ip, + device = userAgent, + token = sessionToken, + ) + + return LoginResponse( + sessionToken = sessionToken, + // Make sure that browser will invalidate session before server + maxAge = sessionTokenMaxAge - 10.seconds, + ) + } + + private companion object { + private val sessionTokenMaxAge = 24.hours } } diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDao.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDao.kt index 16ed384..1204cc9 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDao.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDao.kt @@ -15,7 +15,7 @@ class UserDao( */ suspend fun insertUser( email: String, - password: String, + hash: String, ): String { val objectId = ObjectId() database.getCollection(User.COLLECTION) @@ -23,7 +23,7 @@ class UserDao( User( id = objectId, email = email, - password = password, + hash = hash, authenticatorKey = "", ) ) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDaoModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDaoModule.kt new file mode 100644 index 0000000..148796a --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/user/UserDaoModule.kt @@ -0,0 +1,15 @@ +package io.heapy.komok.business.user + +import io.heapy.komok.dao.mg.MongoModule +import io.heapy.komok.tech.di.lib.Module + +@Module +open class UserDaoModule( + private val mongoModule: MongoModule, +) { + open val userDao by lazy { + UserDao( + database = mongoModule.komokDatabase, + ) + } +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSession.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSession.kt deleted file mode 100644 index f0586fe..0000000 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSession.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.heapy.komok.business.user.session - -import com.mongodb.kotlin.client.coroutine.MongoDatabase -import io.heapy.komok.dao.mg.MongoV1.Session -import org.bson.types.ObjectId -import kotlin.time.Duration.Companion.hours - -class UserSession( - private val database: MongoDatabase, -) { - suspend fun createSession( - userId: ObjectId, - ) { - database.getCollection(Session.COLLECTION) - .insertOne( - Session( - id = ObjectId(), - userId = userId, - expiration = java.time.Instant.now().epochSecond + sessionLifetime.inWholeSeconds, - ip = "", - device = "", - ) - ) - } - - companion object { - val sessionLifetime = 1.hours - } -} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDao.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDao.kt new file mode 100644 index 0000000..f687c34 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDao.kt @@ -0,0 +1,66 @@ +package io.heapy.komok.business.user.session + +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import io.heapy.komok.dao.mg.MongoV1.Session +import io.heapy.komok.infra.http.server.errors.AuthenticationError +import io.heapy.komok.infra.http.server.errors.authenticationError +import io.heapy.komok.infra.time.TimeSource +import kotlinx.coroutines.flow.firstOrNull +import org.bson.types.ObjectId +import kotlin.time.Duration + +class UserSessionDao( + private val database: MongoDatabase, + private val timeSource: TimeSource, +) { + suspend fun createSession( + userId: ObjectId, + maxAge: Duration, + ip: String, + device: String, + token: String, + ) { + database.getCollection(Session.COLLECTION) + .insertOne( + Session( + id = ObjectId(), + userId = userId, + expiration = timeSource.instant().epochSecond + maxAge.inWholeSeconds, + ip = ip, + device = device, + token = token, + ) + ) + } + + suspend fun verifySession( + token: String, + ip: String, + ) { + val session = database + .getCollection(Session.COLLECTION) + .find( + and( + eq( + Session::token.name, + token, + ), + eq( + Session::ip.name, + ip, + ), + ), + ) + .firstOrNull() + + if (session == null) { + authenticationError(AuthenticationError.INVALID_SESSION) + } + + if (session.expiration < timeSource.instant().epochSecond) { + authenticationError(AuthenticationError.SESSION_EXPIRED) + } + } +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDaoModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDaoModule.kt new file mode 100644 index 0000000..fc12bcb --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/UserSessionDaoModule.kt @@ -0,0 +1,18 @@ +package io.heapy.komok.business.user.session + +import io.heapy.komok.dao.mg.MongoModule +import io.heapy.komok.infra.time.TimeSourceModule +import io.heapy.komok.tech.di.lib.Module + +@Module +open class UserSessionDaoModule( + private val mongoModule: MongoModule, + private val timeSourceModule: TimeSourceModule, +) { + open val userSessionDao by lazy { + UserSessionDao( + timeSource = timeSourceModule.timeSource, + database = mongoModule.komokDatabase, + ) + } +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/Argon2id.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/Argon2id.kt similarity index 91% rename from komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/Argon2id.kt rename to komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/Argon2id.kt index c2b8b1e..32fd98a 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/Argon2id.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/Argon2id.kt @@ -1,4 +1,4 @@ -package io.heapy.komok.business.user.argon2 +package io.heapy.komok.infra.argon2 import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -11,22 +11,15 @@ import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi interface PasswordHasher { - fun hash(password: String): String + fun hash( + password: String, + ): String fun verify( password: String, hash: String, ): Boolean } -fun main() { - val hasher = createPasswordHasherModule {} - .passwordHasher - - val hash = hasher.hash("password") - - println(hash) -} - @OptIn(ExperimentalEncodingApi::class) class Argon2idPasswordHasher( private val json: Json, diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/PasswordHasherModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/PasswordHasherModule.kt similarity index 92% rename from komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/PasswordHasherModule.kt rename to komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/PasswordHasherModule.kt index 00d7556..0ea0756 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/user/argon2/PasswordHasherModule.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/argon2/PasswordHasherModule.kt @@ -1,4 +1,4 @@ -package io.heapy.komok.business.user.argon2 +package io.heapy.komok.infra.argon2 import io.heapy.komok.tech.di.lib.Module import kotlinx.serialization.json.Json diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/Defaults.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/Defaults.kt index 28c5d2b..d3703cb 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/Defaults.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/Defaults.kt @@ -2,7 +2,10 @@ package io.heapy.komok.infra.http.server import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm -import io.heapy.komok.business.login.JwtConfiguration +import io.heapy.komok.infra.http.server.errors.AuthenticationException +import io.heapy.komok.infra.http.server.errors.AuthorizationException +import io.heapy.komok.infra.http.server.errors.BadRequestException +import io.heapy.komok.infra.jwt.JwtConfiguration import io.heapy.komok.server.common.KomokServerFeature import io.ktor.http.* import io.ktor.http.content.* @@ -12,11 +15,9 @@ import io.ktor.server.auth.* import io.ktor.server.auth.jwt.* import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.resources.Resources import io.ktor.server.response.* -import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.days class DefaultFeature( @@ -36,10 +37,6 @@ fun Application.defaults( json() } - install(DefaultHeaders) { - header("X-Application", "komok") - } - install(CachingHeaders) { options { _, outgoingContent -> when (outgoingContent.contentType?.withoutParameters()) { @@ -57,14 +54,14 @@ fun Application.defaults( } install(StatusPages) { - exception { call, _ -> - call.respond(HttpStatusCode.Unauthorized) + exception { call, cause -> + call.respond(HttpStatusCode.Unauthorized, cause.response) } - exception { call, _ -> - call.respond(HttpStatusCode.Forbidden) + exception { call, cause -> + call.respond(HttpStatusCode.Forbidden, cause.response) } - exception { call, cause -> - call.respond(HttpStatusCode.BadRequest, cause.fields) + exception { call, cause -> + call.respond(HttpStatusCode.BadRequest, cause.response) } } @@ -88,17 +85,3 @@ fun Application.defaults( } } } - -class AuthenticationException : RuntimeException() - -class AuthorizationException : RuntimeException() - -class ConstraintViolationException( - val fields: List, -) : RuntimeException() - -@Serializable -data class ConstraintViolationFields( - val message: String, - val fields: List, -) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthenticationException.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthenticationException.kt new file mode 100644 index 0000000..2e50f02 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthenticationException.kt @@ -0,0 +1,19 @@ +package io.heapy.komok.infra.http.server.errors + +class AuthenticationException( + val response: AuthenticationError, +) : RuntimeException() + +@Suppress("NOTHING_TO_INLINE") +inline fun authenticationError( + type: AuthenticationError, +): Nothing { + throw AuthenticationException( + response = type, + ) +} + +enum class AuthenticationError { + INVALID_SESSION, + SESSION_EXPIRED, +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthorizationException.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthorizationException.kt new file mode 100644 index 0000000..2227ed0 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/AuthorizationException.kt @@ -0,0 +1,14 @@ +package io.heapy.komok.infra.http.server.errors + +class AuthorizationException( + val response: String, +) : RuntimeException() + +@Suppress("NOTHING_TO_INLINE") +inline fun authorizationError( + message: String, +): Nothing { + throw AuthorizationException( + response = message, + ) +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestException.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestException.kt new file mode 100644 index 0000000..d5afb09 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestException.kt @@ -0,0 +1,44 @@ +package io.heapy.komok.infra.http.server.errors + +class BadRequestException( + val response: BadRequestResponse, +) : RuntimeException() + +@Suppress("NOTHING_TO_INLINE") +inline fun badRequestError( + message: String, +): Nothing { + throw BadRequestException( + response = GenericBadRequestResponse( + message = message, + ), + ) +} + +@Suppress("NOTHING_TO_INLINE") +inline fun badRequestError( + jsonPath: String, + message: String, +): Nothing { + throw BadRequestException( + response = FieldBadRequestResponse( + fields = listOf( + FieldBadRequestResponse.Field( + jsonPath = jsonPath, + message = message, + ), + ), + ), + ) +} + +@Suppress("NOTHING_TO_INLINE") +inline fun badRequestError( + fields: List, +): Nothing { + throw BadRequestException( + response = FieldBadRequestResponse( + fields = fields, + ), + ) +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestResponse.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestResponse.kt new file mode 100644 index 0000000..34b3c93 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/http/server/errors/BadRequestResponse.kt @@ -0,0 +1,22 @@ +package io.heapy.komok.infra.http.server.errors + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface BadRequestResponse + +@Serializable +data class GenericBadRequestResponse( + val message: String, +) : BadRequestResponse + +@Serializable +data class FieldBadRequestResponse( + val fields: List, +) : BadRequestResponse { + @Serializable + data class Field( + val jsonPath: String, + val message: String, + ) +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/login/JwtConfiguration.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtConfiguration.kt similarity index 86% rename from komok-app/src/main/kotlin/io/heapy/komok/business/login/JwtConfiguration.kt rename to komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtConfiguration.kt index 5c22188..6029e40 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/login/JwtConfiguration.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtConfiguration.kt @@ -1,4 +1,4 @@ -package io.heapy.komok.business.login +package io.heapy.komok.infra.jwt import kotlinx.serialization.Serializable import kotlin.time.Duration diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtModule.kt index 5555381..d5ef721 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtModule.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtModule.kt @@ -1,6 +1,5 @@ package io.heapy.komok.infra.jwt -import io.heapy.komok.business.login.JwtConfiguration import io.heapy.komok.infra.time.TimeSourceModule import io.heapy.komok.tech.config.ConfigurationModule import io.heapy.komok.tech.di.lib.Module diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtService.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtService.kt index eda1559..3a0ad6a 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtService.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/jwt/JwtService.kt @@ -4,7 +4,6 @@ import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import io.heapy.komok.infra.time.TimeSource import io.heapy.komok.auth.common.User -import io.heapy.komok.business.login.JwtConfiguration import java.time.temporal.ChronoUnit interface JwtService { diff --git a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/SessionTokenGenerator.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenService.kt similarity index 78% rename from komok-app/src/main/kotlin/io/heapy/komok/business/user/session/SessionTokenGenerator.kt rename to komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenService.kt index 866ab58..9fa3090 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/business/user/session/SessionTokenGenerator.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenService.kt @@ -1,14 +1,14 @@ -package io.heapy.komok.business.user.session +package io.heapy.komok.infra.session_token import java.security.SecureRandom -interface SessionTokenGenerator { +interface SessionTokenService { fun generate(): String } -class SecureRandomSessionTokenGenerator( +class SecureRandomSessionTokenService( private val random: SecureRandom, -) : SessionTokenGenerator { +) : SessionTokenService { override fun generate(): String { return buildString(SESSION_TOKEN_LENGTH) { repeat(SESSION_TOKEN_LENGTH) { diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenServiceModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenServiceModule.kt new file mode 100644 index 0000000..c972e82 --- /dev/null +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/session_token/SessionTokenServiceModule.kt @@ -0,0 +1,17 @@ +package io.heapy.komok.infra.session_token + +import io.heapy.komok.tech.di.lib.Module +import java.security.SecureRandom + +@Module +open class SessionTokenServiceModule { + open val random by lazy { + SecureRandom() + } + + open val sessionTokenService by lazy { + SecureRandomSessionTokenService( + random = random, + ) + } +} diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordModule.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordModule.kt index 396db13..9cec5ef 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordModule.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordModule.kt @@ -9,8 +9,8 @@ open class TimeBasedOneTimePasswordModule( private val base32Module: Base32Module, private val timeSourceModule: TimeSourceModule, ) { - open val timeBasedOneTimePassword by lazy { - TimeBasedOneTimePassword( + open val timeBasedOneTimePasswordService by lazy { + TimeBasedOneTimePasswordService( base32 = base32Module.base32, timeSource = timeSourceModule.timeSource, ) diff --git a/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePassword.kt b/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordService.kt similarity index 94% rename from komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePassword.kt rename to komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordService.kt index 53cc982..6818f37 100644 --- a/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePassword.kt +++ b/komok-app/src/main/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordService.kt @@ -8,7 +8,7 @@ import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.math.pow -class TimeBasedOneTimePassword( +class TimeBasedOneTimePasswordService( private val base32: Base32, private val timeSource: TimeSource, ) { @@ -53,11 +53,11 @@ class TimeBasedOneTimePassword( fun validate( secret: String, - otp: String, + totp: String, ): Boolean { val calculatedOtp = generate(secret) - return calculatedOtp == otp + return calculatedOtp == totp } private companion object { diff --git a/komok-app/src/test/kotlin/io/heapy/komok/business/user/argon2/Argon2idPasswordHasherTest.kt b/komok-app/src/test/kotlin/io/heapy/komok/infra/argon2/Argon2idPasswordHasherTest.kt similarity index 95% rename from komok-app/src/test/kotlin/io/heapy/komok/business/user/argon2/Argon2idPasswordHasherTest.kt rename to komok-app/src/test/kotlin/io/heapy/komok/infra/argon2/Argon2idPasswordHasherTest.kt index 3b2c500..c0b039a 100644 --- a/komok-app/src/test/kotlin/io/heapy/komok/business/user/argon2/Argon2idPasswordHasherTest.kt +++ b/komok-app/src/test/kotlin/io/heapy/komok/infra/argon2/Argon2idPasswordHasherTest.kt @@ -1,4 +1,4 @@ -package io.heapy.komok.business.user.argon2 +package io.heapy.komok.infra.argon2 import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue diff --git a/komok-app/src/test/kotlin/io/heapy/komok/infra/jwt/JwtServiceTest.kt b/komok-app/src/test/kotlin/io/heapy/komok/infra/jwt/JwtServiceTest.kt index 7748eb7..e5f6bbd 100644 --- a/komok-app/src/test/kotlin/io/heapy/komok/infra/jwt/JwtServiceTest.kt +++ b/komok-app/src/test/kotlin/io/heapy/komok/infra/jwt/JwtServiceTest.kt @@ -4,7 +4,6 @@ import io.heapy.komok.KomokBaseTest import io.heapy.komok.TestTimeSource import io.heapy.komok.UnitTest import io.heapy.komok.auth.common.User -import io.heapy.komok.business.login.JwtConfiguration import io.heapy.komok.tech.config.buildMockKomokConfiguration import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.assertThrows diff --git a/komok-app/src/test/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordTest.kt b/komok-app/src/test/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordTest.kt index 0f022d5..a35483f 100644 --- a/komok-app/src/test/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordTest.kt +++ b/komok-app/src/test/kotlin/io/heapy/komok/infra/totp/TimeBasedOneTimePasswordTest.kt @@ -21,7 +21,7 @@ class TimeBasedOneTimePasswordTest { new = Instant.ofEpochSecond(1720159128), ) - val totp = module.timeBasedOneTimePassword + val totp = module.timeBasedOneTimePasswordService .generate("JBSWY3DPEHPK3PXP") assertEquals( @@ -45,10 +45,10 @@ class TimeBasedOneTimePasswordTest { new = Instant.ofEpochSecond(1720159128), ) - val decision1 = module.timeBasedOneTimePassword + val decision1 = module.timeBasedOneTimePasswordService .validate( secret = "JBSWY3DPEHPK3PXP", - otp = "476288", + totp = "476288", ) assertTrue(decision1) @@ -57,10 +57,10 @@ class TimeBasedOneTimePasswordTest { new = Instant.ofEpochSecond(1720159158), ) - val decision2 = module.timeBasedOneTimePassword + val decision2 = module.timeBasedOneTimePasswordService .validate( secret = "JBSWY3DPEHPK3PXP", - otp = "476288", + totp = "476288", ) assertFalse(decision2) diff --git a/komok-dao-mg/src/main/kotlin/io/heapy/komok/dao/mg/MongoV1.kt b/komok-dao-mg/src/main/kotlin/io/heapy/komok/dao/mg/MongoV1.kt index 8118a4d..14a316e 100644 --- a/komok-dao-mg/src/main/kotlin/io/heapy/komok/dao/mg/MongoV1.kt +++ b/komok-dao-mg/src/main/kotlin/io/heapy/komok/dao/mg/MongoV1.kt @@ -7,7 +7,7 @@ object MongoV1 { data class User( @BsonId val id: ObjectId, val email: String, - val password: String, + val hash: String, val authenticatorKey: String, ) { companion object { @@ -21,6 +21,7 @@ object MongoV1 { val expiration: Long, val ip: String, val device: String, + val token: String, ) { companion object { const val COLLECTION = "session"