implement secret regeneration
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.2.10"
|
kotlin = "2.2.10"
|
||||||
ktor = "3.1.3"
|
ktor = "3.2.3"
|
||||||
exposed = "0.60.0"
|
exposed = "0.60.0"
|
||||||
postgres = "42.7.5"
|
postgres = "42.7.5"
|
||||||
koin = "4.0.3"
|
koin = "4.1.0"
|
||||||
random = "1.0.0"
|
random = "1.0.0"
|
||||||
otp = "1.1.1"
|
otp = "1.1.1"
|
||||||
kdf = "1.0.3"
|
kdf = "1.0.3"
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package com.infendro.account.config
|
package com.infendro.account.config
|
||||||
|
|
||||||
import com.infendro.account.model.migrate
|
import com.infendro.account.model.Role
|
||||||
import io.ktor.server.application.Application
|
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.Database
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.koin.ktor.ext.inject
|
||||||
|
|
||||||
fun Application.configureDatabase() {
|
fun Application.configureDatabase() {
|
||||||
val config = environment.config.database
|
val config = environment.config.database
|
||||||
@@ -14,3 +21,63 @@ fun Application.configureDatabase() {
|
|||||||
)
|
)
|
||||||
migrate()
|
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 io.ktor.server.application.*
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koin.ktor.plugin.koin
|
import org.koin.ktor.plugin.Koin
|
||||||
import org.koin.logger.slf4jLogger
|
import org.koin.logger.slf4jLogger
|
||||||
|
|
||||||
fun Application.configureDependencyInjection() {
|
fun Application.configureDependencyInjection() {
|
||||||
koin {
|
install(Koin) {
|
||||||
slf4jLogger()
|
slf4jLogger()
|
||||||
modules(module)
|
modules(module)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ fun Application.configureSecurity() {
|
|||||||
val config = environment.config.security
|
val config = environment.config.security
|
||||||
|
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
cookie<AuthenticationSession>(
|
cookie<AuthenticationSession>("Authentication") {
|
||||||
"Authentication",
|
|
||||||
SessionStorageMemory(),
|
|
||||||
) {
|
|
||||||
cookie.domain = config.cookie.domain
|
cookie.domain = config.cookie.domain
|
||||||
cookie.path = config.cookie.path
|
cookie.path = config.cookie.path
|
||||||
cookie.httpOnly = true
|
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
|
package com.infendro.account.model
|
||||||
|
|
||||||
import com.infendro.account.model.entity.MigrationTable
|
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.model.repository.MigrationRepository
|
||||||
import com.infendro.account.util.SecureHasher
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.Transaction
|
import org.jetbrains.exposed.sql.Transaction
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.koin.ktor.ext.inject
|
import org.koin.ktor.ext.inject
|
||||||
|
|
||||||
fun Application.migrate() {
|
fun Application.migration(
|
||||||
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(
|
|
||||||
name: String,
|
name: String,
|
||||||
block: Transaction.() -> Unit,
|
block: Transaction.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
package com.infendro.account.routing
|
package com.infendro.account.routing
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.AuthenticationPrincipal
|
||||||
import com.infendro.account.dto.request.PostAccountRequest
|
import com.infendro.account.dto.request.*
|
||||||
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.service.AccountService
|
import com.infendro.account.service.AccountService
|
||||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||||
import io.ktor.resources.Resource
|
import io.ktor.resources.*
|
||||||
import io.ktor.server.auth.authenticate
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.auth.principal
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.request.receive
|
import io.ktor.server.resources.*
|
||||||
import io.ktor.server.resources.delete
|
|
||||||
import io.ktor.server.resources.get
|
|
||||||
import io.ktor.server.resources.post
|
import io.ktor.server.resources.post
|
||||||
import io.ktor.server.resources.put
|
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.routing.Routing
|
||||||
import io.ktor.server.sessions.sessions
|
import io.ktor.server.sessions.*
|
||||||
import org.koin.ktor.ext.inject
|
import org.koin.ktor.ext.inject
|
||||||
|
|
||||||
@Resource("/account")
|
@Resource("/account")
|
||||||
@@ -41,6 +36,11 @@ private class Account {
|
|||||||
val parent: Current,
|
val parent: Current,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Resource("/secret")
|
||||||
|
class Secret(
|
||||||
|
val parent: Current,
|
||||||
|
)
|
||||||
|
|
||||||
@Resource("/session")
|
@Resource("/session")
|
||||||
class Session(
|
class Session(
|
||||||
val parent: Current,
|
val parent: Current,
|
||||||
@@ -118,6 +118,14 @@ fun Routing.account() {
|
|||||||
call.respond(OK)
|
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> {
|
get<Account.Current.Session.All> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<AuthenticationPrincipal>()!!
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,15 @@ package com.infendro.account.service
|
|||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.AuthenticationPrincipal
|
||||||
import com.infendro.account.config.AuthenticationSession
|
import com.infendro.account.config.AuthenticationSession
|
||||||
import com.infendro.account.dto.request.PostAccountRequest
|
import com.infendro.account.dto.request.*
|
||||||
import com.infendro.account.dto.request.PutAccountCurrentPasswordRequest
|
import com.infendro.account.dto.response.*
|
||||||
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.exception.client.ConflictException
|
import com.infendro.account.exception.client.ConflictException
|
||||||
import com.infendro.account.exception.client.ForbiddenException
|
import com.infendro.account.exception.client.ForbiddenException
|
||||||
import com.infendro.account.exception.client.NotFoundException
|
import com.infendro.account.exception.client.NotFoundException
|
||||||
import com.infendro.account.exception.client.UnauthorizedException
|
import com.infendro.account.exception.client.UnauthorizedException
|
||||||
import com.infendro.account.model.Role.OWNER
|
import com.infendro.account.model.Role.OWNER
|
||||||
import com.infendro.account.model.Role.USER
|
import com.infendro.account.model.Role.USER
|
||||||
import com.infendro.account.model.entity.AccessTable
|
import com.infendro.account.model.entity.*
|
||||||
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.repository.AccessRepository
|
import com.infendro.account.model.repository.AccessRepository
|
||||||
import com.infendro.account.model.repository.AccountRepository
|
import com.infendro.account.model.repository.AccountRepository
|
||||||
import com.infendro.account.model.repository.SessionRepository
|
import com.infendro.account.model.repository.SessionRepository
|
||||||
@@ -77,9 +68,7 @@ class AccountService(
|
|||||||
fun getCurrent(
|
fun getCurrent(
|
||||||
principal: AuthenticationPrincipal,
|
principal: AuthenticationPrincipal,
|
||||||
): AccountResponse {
|
): AccountResponse {
|
||||||
val (account, _) = principal
|
return principal.account.toResponse()
|
||||||
|
|
||||||
return account.toResponse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCurrent(
|
fun deleteCurrent(
|
||||||
@@ -105,7 +94,6 @@ class AccountService(
|
|||||||
sessionRepository.delete(principal.account.sessions())
|
sessionRepository.delete(principal.account.sessions())
|
||||||
|
|
||||||
sessions.clear<AuthenticationSession>()
|
sessions.clear<AuthenticationSession>()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCurrentPassword(
|
fun putCurrentPassword(
|
||||||
@@ -127,11 +115,32 @@ class AccountService(
|
|||||||
sessions.clear<AuthenticationSession>()
|
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(
|
fun getCurrentSessionAll(
|
||||||
principal: AuthenticationPrincipal,
|
principal: AuthenticationPrincipal,
|
||||||
): List<SessionResponse> {
|
): List<SessionResponse> {
|
||||||
return principal.account.sessions()
|
return principal.account.sessions()
|
||||||
.map { it.toResponse() }
|
.map(SessionEntity::toResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCurrentSessionId(
|
fun deleteCurrentSessionId(
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ import com.infendro.account.model.Role
|
|||||||
class RoleService {
|
class RoleService {
|
||||||
fun getAll(): List<RoleResponse> {
|
fun getAll(): List<RoleResponse> {
|
||||||
return Role.ALL
|
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.exception.client.UnauthorizedException
|
||||||
import com.infendro.account.model.Role.USER
|
import com.infendro.account.model.Role.USER
|
||||||
import com.infendro.account.model.entity.AccountTable
|
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.entity.SessionTable
|
||||||
import com.infendro.account.model.repository.AccountRepository
|
import com.infendro.account.model.repository.AccountRepository
|
||||||
import com.infendro.account.model.repository.SessionRepository
|
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.OTP
|
||||||
import com.infendro.account.util.SecureHasher
|
import com.infendro.account.util.SecureHasher
|
||||||
import com.infendro.account.util.TokenGenerator
|
import com.infendro.account.util.TokenGenerator
|
||||||
import io.ktor.server.sessions.CurrentSession
|
import io.ktor.server.sessions.*
|
||||||
import io.ktor.server.sessions.clear
|
|
||||||
import io.ktor.server.sessions.set
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||||
|
|
||||||
@@ -47,7 +46,9 @@ class SessionService(
|
|||||||
this.account = account
|
this.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.set(AuthenticationSession(token))
|
sessions.set(
|
||||||
|
AuthenticationSession(token)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(
|
fun getAll(
|
||||||
@@ -59,7 +60,7 @@ class SessionService(
|
|||||||
return accountRepository
|
return accountRepository
|
||||||
.all { AccountTable.role inList principal.role.children }
|
.all { AccountTable.role inList principal.role.children }
|
||||||
.flatMap { it.sessions() }
|
.flatMap { it.sessions() }
|
||||||
.map { it.toResponse() }
|
.map(SessionEntity::toResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteId(
|
fun deleteId(
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ object Hasher {
|
|||||||
value: String,
|
value: String,
|
||||||
): String {
|
): String {
|
||||||
val bytes = SHA256.hash(value.toByteArray())
|
val bytes = SHA256.hash(value.toByteArray())
|
||||||
return Hex.encode(bytes).toString()
|
return Hex.encode(bytes).decodeToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user