implement secret regeneration
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<AccountRepository>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -34,10 +34,7 @@ fun Application.configureSecurity() {
|
||||
val config = environment.config.security
|
||||
|
||||
install(Sessions) {
|
||||
cookie<AuthenticationSession>(
|
||||
"Authentication",
|
||||
SessionStorageMemory(),
|
||||
) {
|
||||
cookie<AuthenticationSession>("Authentication") {
|
||||
cookie.domain = config.cookie.domain
|
||||
cookie.path = config.cookie.path
|
||||
cookie.httpOnly = true
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.infendro.account.dto.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PostAccountCurrentSecretRequest(
|
||||
val password: String,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.infendro.account.dto.response
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PostAccountCurrentSecretResponse(
|
||||
val secret: String,
|
||||
)
|
||||
@@ -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<AccountRepository>()
|
||||
|
||||
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,
|
||||
) {
|
||||
|
||||
@@ -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<Account.Current.Secret> {
|
||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
||||
val request = call.receive<PostAccountCurrentSecretRequest>()
|
||||
|
||||
accountService.postCurrentSecret(principal, call.sessions, request)
|
||||
.also { call.respond(it) }
|
||||
}
|
||||
|
||||
get<Account.Current.Session.All> {
|
||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
||||
|
||||
|
||||
@@ -56,4 +56,4 @@ class AccessService(
|
||||
|
||||
accessRepository.delete(access)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AuthenticationSession>()
|
||||
|
||||
}
|
||||
|
||||
fun putCurrentPassword(
|
||||
@@ -127,11 +115,32 @@ class AccountService(
|
||||
sessions.clear<AuthenticationSession>()
|
||||
}
|
||||
|
||||
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<AuthenticationSession>()
|
||||
|
||||
return PostAccountCurrentSecretResponse(
|
||||
secret = secret
|
||||
)
|
||||
}
|
||||
|
||||
fun getCurrentSessionAll(
|
||||
principal: AuthenticationPrincipal,
|
||||
): List<SessionResponse> {
|
||||
return principal.account.sessions()
|
||||
.map { it.toResponse() }
|
||||
.map(SessionEntity::toResponse)
|
||||
}
|
||||
|
||||
fun deleteCurrentSessionId(
|
||||
|
||||
@@ -7,6 +7,6 @@ import com.infendro.account.model.Role
|
||||
class RoleService {
|
||||
fun getAll(): List<RoleResponse> {
|
||||
return Role.ALL
|
||||
.map { it.toResponse() }
|
||||
.map(Role::toResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user