From 7beeb3961659035250566001c2c60673b7dad18a Mon Sep 17 00:00:00 2001 From: Infendro Date: Tue, 19 Aug 2025 23:26:05 +0200 Subject: [PATCH] implement secret regeneration --- backend/gradle/libs.versions.toml | 4 +- .../com/infendro/account/config/Database.kt | 71 ++++++++++++++++++- .../account/config/DependencyInjection.kt | 4 +- .../com/infendro/account/config/Security.kt | 5 +- .../PostAccountCurrentSecretRequest.kt | 8 +++ .../PostAccountCurrentSecretResponse.kt | 8 +++ .../com/infendro/account/model/Migration.kt | 65 +---------------- .../com/infendro/account/routing/Account.kt | 32 +++++---- .../infendro/account/service/AccessService.kt | 2 +- .../account/service/AccountService.kt | 43 ++++++----- .../infendro/account/service/RoleService.kt | 2 +- .../account/service/SessionService.kt | 11 +-- .../com/infendro/account/util/Hasher.kt | 2 +- 13 files changed, 146 insertions(+), 111 deletions(-) create mode 100644 backend/src/main/kotlin/com/infendro/account/dto/request/PostAccountCurrentSecretRequest.kt create mode 100644 backend/src/main/kotlin/com/infendro/account/dto/response/PostAccountCurrentSecretResponse.kt diff --git a/backend/gradle/libs.versions.toml b/backend/gradle/libs.versions.toml index fa9dd82..3024f99 100644 --- a/backend/gradle/libs.versions.toml +++ b/backend/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] kotlin = "2.2.10" -ktor = "3.1.3" +ktor = "3.2.3" exposed = "0.60.0" postgres = "42.7.5" -koin = "4.0.3" +koin = "4.1.0" random = "1.0.0" otp = "1.1.1" kdf = "1.0.3" diff --git a/backend/src/main/kotlin/com/infendro/account/config/Database.kt b/backend/src/main/kotlin/com/infendro/account/config/Database.kt index 2dffe9c..167520a 100644 --- a/backend/src/main/kotlin/com/infendro/account/config/Database.kt +++ b/backend/src/main/kotlin/com/infendro/account/config/Database.kt @@ -1,8 +1,15 @@ package com.infendro.account.config -import com.infendro.account.model.migrate -import io.ktor.server.application.Application +import com.infendro.account.model.Role +import com.infendro.account.model.entity.MigrationTable +import com.infendro.account.model.migration +import com.infendro.account.model.repository.AccountRepository +import com.infendro.account.util.SecureHasher +import io.ktor.server.application.* import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction +import org.koin.ktor.ext.inject fun Application.configureDatabase() { val config = environment.config.database @@ -14,3 +21,63 @@ fun Application.configureDatabase() { ) migrate() } + +fun Application.migrate() { + transaction { + SchemaUtils.create(MigrationTable) + } + + val accounts by inject() + + migration("initial") { + exec( + """ + CREATE TABLE access + ( + id BIGSERIAL + PRIMARY KEY, + token_hash TEXT + NOT NULL + ); + + CREATE TABLE account + ( + id BIGSERIAL + PRIMARY KEY, + username TEXT + UNIQUE + NOT NULL, + password_hash TEXT + NOT NULL, + password_salt TEXT + NOT NULL, + secret TEXT + NOT NULL, + role TEXT + NOT NULL + ); + + CREATE TABLE session + ( + id BIGSERIAL + PRIMARY KEY, + token_hash TEXT + NOT NULL, + account_id BIGINT + REFERENCES account (id) ON DELETE CASCADE + NOT NULL + ); + """.trimIndent() + ) + + accounts.insert { + val salt = SecureHasher.generateSalt() + + this.username = "infendro" + this.passwordHash = SecureHasher.hash("password", salt) + this.passwordSalt = salt + this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA====" + this.role = Role.OWNER + } + } +} diff --git a/backend/src/main/kotlin/com/infendro/account/config/DependencyInjection.kt b/backend/src/main/kotlin/com/infendro/account/config/DependencyInjection.kt index 298233e..adf927c 100644 --- a/backend/src/main/kotlin/com/infendro/account/config/DependencyInjection.kt +++ b/backend/src/main/kotlin/com/infendro/account/config/DependencyInjection.kt @@ -11,11 +11,11 @@ import com.infendro.account.service.SessionService import io.ktor.server.application.* import org.koin.core.module.dsl.singleOf import org.koin.dsl.module -import org.koin.ktor.plugin.koin +import org.koin.ktor.plugin.Koin import org.koin.logger.slf4jLogger fun Application.configureDependencyInjection() { - koin { + install(Koin) { slf4jLogger() modules(module) } diff --git a/backend/src/main/kotlin/com/infendro/account/config/Security.kt b/backend/src/main/kotlin/com/infendro/account/config/Security.kt index ef2ad23..0ebc692 100644 --- a/backend/src/main/kotlin/com/infendro/account/config/Security.kt +++ b/backend/src/main/kotlin/com/infendro/account/config/Security.kt @@ -34,10 +34,7 @@ fun Application.configureSecurity() { val config = environment.config.security install(Sessions) { - cookie( - "Authentication", - SessionStorageMemory(), - ) { + cookie("Authentication") { cookie.domain = config.cookie.domain cookie.path = config.cookie.path cookie.httpOnly = true diff --git a/backend/src/main/kotlin/com/infendro/account/dto/request/PostAccountCurrentSecretRequest.kt b/backend/src/main/kotlin/com/infendro/account/dto/request/PostAccountCurrentSecretRequest.kt new file mode 100644 index 0000000..edf5f04 --- /dev/null +++ b/backend/src/main/kotlin/com/infendro/account/dto/request/PostAccountCurrentSecretRequest.kt @@ -0,0 +1,8 @@ +package com.infendro.account.dto.request + +import kotlinx.serialization.Serializable + +@Serializable +data class PostAccountCurrentSecretRequest( + val password: String, +) diff --git a/backend/src/main/kotlin/com/infendro/account/dto/response/PostAccountCurrentSecretResponse.kt b/backend/src/main/kotlin/com/infendro/account/dto/response/PostAccountCurrentSecretResponse.kt new file mode 100644 index 0000000..d2c462f --- /dev/null +++ b/backend/src/main/kotlin/com/infendro/account/dto/response/PostAccountCurrentSecretResponse.kt @@ -0,0 +1,8 @@ +package com.infendro.account.dto.response + +import kotlinx.serialization.Serializable + +@Serializable +data class PostAccountCurrentSecretResponse( + val secret: String, +) diff --git a/backend/src/main/kotlin/com/infendro/account/model/Migration.kt b/backend/src/main/kotlin/com/infendro/account/model/Migration.kt index df8b527..642adfd 100644 --- a/backend/src/main/kotlin/com/infendro/account/model/Migration.kt +++ b/backend/src/main/kotlin/com/infendro/account/model/Migration.kt @@ -1,77 +1,14 @@ package com.infendro.account.model import com.infendro.account.model.entity.MigrationTable -import com.infendro.account.model.repository.AccountRepository import com.infendro.account.model.repository.MigrationRepository -import com.infendro.account.util.SecureHasher import io.ktor.server.application.* -import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.transactions.transaction import org.koin.ktor.ext.inject -fun Application.migrate() { - transaction { - SchemaUtils.create(MigrationTable) - } - - val accounts by inject() - - migration("initial") { - exec( - """ - CREATE TABLE access - ( - id BIGSERIAL - PRIMARY KEY, - token_hash TEXT - NOT NULL - ); - - CREATE TABLE account - ( - id BIGSERIAL - PRIMARY KEY, - username TEXT - UNIQUE - NOT NULL, - password_hash TEXT - NOT NULL, - password_salt TEXT - NOT NULL, - secret TEXT - NOT NULL, - role TEXT - NOT NULL - ); - - CREATE TABLE session - ( - id BIGSERIAL - PRIMARY KEY, - token_hash TEXT - NOT NULL, - account_id BIGINT - REFERENCES account (id) ON DELETE CASCADE - NOT NULL - ); - """.trimIndent() - ) - - accounts.insert { - val salt = SecureHasher.generateSalt() - - this.username = "infendro" - this.passwordHash = SecureHasher.hash("password", salt) - this.passwordSalt = salt - this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA====" - this.role = Role.OWNER - } - } -} - -private fun Application.migration( +fun Application.migration( name: String, block: Transaction.() -> Unit, ) { diff --git a/backend/src/main/kotlin/com/infendro/account/routing/Account.kt b/backend/src/main/kotlin/com/infendro/account/routing/Account.kt index 5a3593c..3603c77 100644 --- a/backend/src/main/kotlin/com/infendro/account/routing/Account.kt +++ b/backend/src/main/kotlin/com/infendro/account/routing/Account.kt @@ -1,23 +1,18 @@ package com.infendro.account.routing import com.infendro.account.config.AuthenticationPrincipal -import com.infendro.account.dto.request.PostAccountRequest -import com.infendro.account.dto.request.PutAccountCurrentPasswordRequest -import com.infendro.account.dto.request.PutAccountCurrentUsernameRequest -import com.infendro.account.dto.request.PutAccountIdRoleRequest +import com.infendro.account.dto.request.* import com.infendro.account.service.AccountService import io.ktor.http.HttpStatusCode.Companion.OK -import io.ktor.resources.Resource -import io.ktor.server.auth.authenticate -import io.ktor.server.auth.principal -import io.ktor.server.request.receive -import io.ktor.server.resources.delete -import io.ktor.server.resources.get +import io.ktor.resources.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.resources.* import io.ktor.server.resources.post import io.ktor.server.resources.put -import io.ktor.server.response.respond +import io.ktor.server.response.* import io.ktor.server.routing.Routing -import io.ktor.server.sessions.sessions +import io.ktor.server.sessions.* import org.koin.ktor.ext.inject @Resource("/account") @@ -41,6 +36,11 @@ private class Account { val parent: Current, ) + @Resource("/secret") + class Secret( + val parent: Current, + ) + @Resource("/session") class Session( val parent: Current, @@ -118,6 +118,14 @@ fun Routing.account() { call.respond(OK) } + post { + val principal = call.principal()!! + val request = call.receive() + + accountService.postCurrentSecret(principal, call.sessions, request) + .also { call.respond(it) } + } + get { val principal = call.principal()!! diff --git a/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt b/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt index 6af89f2..d4bd2e2 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt @@ -56,4 +56,4 @@ class AccessService( accessRepository.delete(access) } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt b/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt index 11f3677..36c8d6d 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt @@ -2,24 +2,15 @@ package com.infendro.account.service import com.infendro.account.config.AuthenticationPrincipal import com.infendro.account.config.AuthenticationSession -import com.infendro.account.dto.request.PostAccountRequest -import com.infendro.account.dto.request.PutAccountCurrentPasswordRequest -import com.infendro.account.dto.request.PutAccountCurrentUsernameRequest -import com.infendro.account.dto.request.PutAccountIdRoleRequest -import com.infendro.account.dto.response.AccountResponse -import com.infendro.account.dto.response.PostAccountResponse -import com.infendro.account.dto.response.SessionResponse -import com.infendro.account.dto.response.toResponse +import com.infendro.account.dto.request.* +import com.infendro.account.dto.response.* import com.infendro.account.exception.client.ConflictException import com.infendro.account.exception.client.ForbiddenException import com.infendro.account.exception.client.NotFoundException import com.infendro.account.exception.client.UnauthorizedException import com.infendro.account.model.Role.OWNER import com.infendro.account.model.Role.USER -import com.infendro.account.model.entity.AccessTable -import com.infendro.account.model.entity.AccountEntity -import com.infendro.account.model.entity.AccountTable -import com.infendro.account.model.entity.SessionTable +import com.infendro.account.model.entity.* import com.infendro.account.model.repository.AccessRepository import com.infendro.account.model.repository.AccountRepository import com.infendro.account.model.repository.SessionRepository @@ -77,9 +68,7 @@ class AccountService( fun getCurrent( principal: AuthenticationPrincipal, ): AccountResponse { - val (account, _) = principal - - return account.toResponse() + return principal.account.toResponse() } fun deleteCurrent( @@ -105,7 +94,6 @@ class AccountService( sessionRepository.delete(principal.account.sessions()) sessions.clear() - } fun putCurrentPassword( @@ -127,11 +115,32 @@ class AccountService( sessions.clear() } + fun postCurrentSecret( + principal: AuthenticationPrincipal, + sessions: CurrentSession, + request: PostAccountCurrentSecretRequest, + ): PostAccountCurrentSecretResponse { + if (principal.account.passwordHash != SecureHasher.hash(request.password, principal.account.passwordSalt)) + throw UnauthorizedException() + + val secret = OTP.generateSecret() + accountRepository.update(principal.account) { + this.secret = secret + } + sessionRepository.delete(principal.account.sessions()) + + sessions.clear() + + return PostAccountCurrentSecretResponse( + secret = secret + ) + } + fun getCurrentSessionAll( principal: AuthenticationPrincipal, ): List { return principal.account.sessions() - .map { it.toResponse() } + .map(SessionEntity::toResponse) } fun deleteCurrentSessionId( diff --git a/backend/src/main/kotlin/com/infendro/account/service/RoleService.kt b/backend/src/main/kotlin/com/infendro/account/service/RoleService.kt index ef01a69..cda1760 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/RoleService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/RoleService.kt @@ -7,6 +7,6 @@ import com.infendro.account.model.Role class RoleService { fun getAll(): List { return Role.ALL - .map { it.toResponse() } + .map(Role::toResponse) } } diff --git a/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt b/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt index b3c0f5a..f612f70 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt @@ -10,6 +10,7 @@ import com.infendro.account.exception.client.NotFoundException import com.infendro.account.exception.client.UnauthorizedException import com.infendro.account.model.Role.USER import com.infendro.account.model.entity.AccountTable +import com.infendro.account.model.entity.SessionEntity import com.infendro.account.model.entity.SessionTable import com.infendro.account.model.repository.AccountRepository import com.infendro.account.model.repository.SessionRepository @@ -17,9 +18,7 @@ import com.infendro.account.util.Hasher import com.infendro.account.util.OTP import com.infendro.account.util.SecureHasher import com.infendro.account.util.TokenGenerator -import io.ktor.server.sessions.CurrentSession -import io.ktor.server.sessions.clear -import io.ktor.server.sessions.set +import io.ktor.server.sessions.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList @@ -47,7 +46,9 @@ class SessionService( this.account = account } - sessions.set(AuthenticationSession(token)) + sessions.set( + AuthenticationSession(token) + ) } fun getAll( @@ -59,7 +60,7 @@ class SessionService( return accountRepository .all { AccountTable.role inList principal.role.children } .flatMap { it.sessions() } - .map { it.toResponse() } + .map(SessionEntity::toResponse) } fun deleteId( diff --git a/backend/src/main/kotlin/com/infendro/account/util/Hasher.kt b/backend/src/main/kotlin/com/infendro/account/util/Hasher.kt index 98c77bc..be306a2 100644 --- a/backend/src/main/kotlin/com/infendro/account/util/Hasher.kt +++ b/backend/src/main/kotlin/com/infendro/account/util/Hasher.kt @@ -8,6 +8,6 @@ object Hasher { value: String, ): String { val bytes = SHA256.hash(value.toByteArray()) - return Hex.encode(bytes).toString() + return Hex.encode(bytes).decodeToString() } }