diff --git a/backend/src/main/kotlin/com/infendro/account/Application.kt b/backend/src/main/kotlin/com/infendro/account/Application.kt index 75a9f1a..b26c4ea 100644 --- a/backend/src/main/kotlin/com/infendro/account/Application.kt +++ b/backend/src/main/kotlin/com/infendro/account/Application.kt @@ -1,6 +1,7 @@ package com.infendro.account import com.infendro.account.config.* +import com.infendro.account.config.configureSession import io.ktor.server.application.* import io.ktor.server.cio.* @@ -13,6 +14,7 @@ fun Application.module() { configureValidation() configureException() + configureSession() configureSecurity() configureRouting() } 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 2449ab5..04e6f6c 100644 --- a/backend/src/main/kotlin/com/infendro/account/config/Database.kt +++ b/backend/src/main/kotlin/com/infendro/account/config/Database.kt @@ -1,9 +1,7 @@ package com.infendro.account.config -import com.infendro.account.model.Role -import com.infendro.account.model.repository.AccountRepository -import com.infendro.account.util.PasswordHasher import com.infendro.ktor.migration.Migration +import initial import io.ktor.server.application.* import org.jetbrains.exposed.v1.jdbc.Database @@ -17,56 +15,6 @@ fun Application.configureDatabase() { ) install(Migration) { - 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(), - ) - - AccountRepository.insert { - val salt = PasswordHasher.generateSalt() - - this.username = "infendro" - this.passwordHash = PasswordHasher.hash("password", salt) - this.passwordSalt = salt - this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA====" - this.role = Role.OWNER - } - } + initial() } } 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 c944f37..3abcb30 100644 --- a/backend/src/main/kotlin/com/infendro/account/config/Security.kt +++ b/backend/src/main/kotlin/com/infendro/account/config/Security.kt @@ -1,25 +1,14 @@ package com.infendro.account.config -import com.infendro.account.exception.client.UnauthorizedException import com.infendro.account.model.Role import com.infendro.account.model.entity.AccountEntity import com.infendro.account.model.entity.SessionEntity -import com.infendro.account.model.entity.SessionTable import com.infendro.account.model.repository.SessionRepository -import com.infendro.account.util.Hasher import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.sessions.* -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.v1.core.eq -import kotlin.time.Duration.Companion.days -@Serializable -data class AuthenticationSession( - val token: String, -) - -data class AuthenticationPrincipal( +data class Principal( val account: AccountEntity, val session: SessionEntity, ) { @@ -28,34 +17,20 @@ data class AuthenticationPrincipal( } fun Application.configureSecurity() { - val config = configuration - - install(Sessions) { - cookie("Authentication") { - this.cookie.domain = config.security.cookie.domain - this.cookie.path = config.security.cookie.path - this.cookie.httpOnly = true - this.cookie.secure = config.security.cookie.secure - this.cookie.extensions["SameSite"] = "Strict" - this.cookie.maxAge = 7.days - } - } install(Authentication) { - session { - validate { session -> - val session = SessionRepository - .singleOrNull { SessionTable.tokenHash eq Hasher.hash(session.token) } - ?: return@validate null - - sessions.set(session) - - AuthenticationPrincipal(session.account(), session) + bearer { + authenticate { credential -> + SessionRepository.by(credential.token)?.principal } - challenge { - call.sessions.clear() - - throw UnauthorizedException() + } + session { + validate { session -> + SessionRepository.by(session.token)?.principal + ?.also { sessions.set(session) } } } } } + +val SessionEntity.principal: Principal + get() = Principal(account(), this) diff --git a/backend/src/main/kotlin/com/infendro/account/config/Session.kt b/backend/src/main/kotlin/com/infendro/account/config/Session.kt new file mode 100644 index 0000000..c59379e --- /dev/null +++ b/backend/src/main/kotlin/com/infendro/account/config/Session.kt @@ -0,0 +1,26 @@ +package com.infendro.account.config + +import io.ktor.server.application.* +import io.ktor.server.sessions.* +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.days + +@Serializable +data class Session( + val token: String, +) + +fun Application.configureSession() { + val config = configuration + + install(Sessions) { + cookie("Authentication") { + this.cookie.domain = config.security.cookie.domain + this.cookie.path = config.security.cookie.path + this.cookie.httpOnly = true + this.cookie.secure = config.security.cookie.secure + this.cookie.extensions["SameSite"] = "Strict" + this.cookie.maxAge = 7.days + } + } +} diff --git a/backend/src/main/kotlin/com/infendro/account/migration/Initial.kt b/backend/src/main/kotlin/com/infendro/account/migration/Initial.kt new file mode 100644 index 0000000..7ac2a83 --- /dev/null +++ b/backend/src/main/kotlin/com/infendro/account/migration/Initial.kt @@ -0,0 +1,56 @@ +import com.infendro.account.model.Role +import com.infendro.account.model.repository.AccountRepository +import com.infendro.account.util.PasswordHasher +import com.infendro.ktor.migration.MigrationConfig + +fun MigrationConfig.initial() = 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(), + ) + + AccountRepository.insert { + val salt = PasswordHasher.generateSalt() + + this.username = "infendro" + this.passwordHash = PasswordHasher.hash("password", salt) + this.passwordSalt = salt + this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA====" + this.role = Role.OWNER + } +} diff --git a/backend/src/main/kotlin/com/infendro/account/model/repository/SessionRepository.kt b/backend/src/main/kotlin/com/infendro/account/model/repository/SessionRepository.kt index 5ef4668..13805b0 100644 --- a/backend/src/main/kotlin/com/infendro/account/model/repository/SessionRepository.kt +++ b/backend/src/main/kotlin/com/infendro/account/model/repository/SessionRepository.kt @@ -1,5 +1,10 @@ package com.infendro.account.model.repository import com.infendro.account.model.entity.SessionEntity +import com.infendro.account.model.entity.SessionTable +import com.infendro.account.util.Hasher +import org.jetbrains.exposed.v1.core.eq -object SessionRepository : Repository(SessionEntity) +object SessionRepository : Repository(SessionEntity) { + fun by(token: String) = singleOrNull { SessionTable.tokenHash eq Hasher.hash(token) } +} diff --git a/backend/src/main/kotlin/com/infendro/account/routing/Access.kt b/backend/src/main/kotlin/com/infendro/account/routing/Access.kt index 7b45d9a..4de4485 100644 --- a/backend/src/main/kotlin/com/infendro/account/routing/Access.kt +++ b/backend/src/main/kotlin/com/infendro/account/routing/Access.kt @@ -1,6 +1,6 @@ package com.infendro.account.routing -import com.infendro.account.config.AuthenticationPrincipal +import com.infendro.account.config.Principal import com.infendro.account.service.AccessService import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.resources.* @@ -27,21 +27,21 @@ private class Access { fun Routing.access() { authenticate { post { - val principal = call.principal()!! + val principal = call.principal()!! AccessService.post(principal) .also { call.respond(it) } } get { - val principal = call.principal()!! + val principal = call.principal()!! AccessService.getAll(principal) .also { call.respond(it) } } delete { resource -> - val principal = call.principal()!! + val principal = call.principal()!! val id = resource.id AccessService.deleteId(principal, id) 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 14a7e1a..b962258 100644 --- a/backend/src/main/kotlin/com/infendro/account/routing/Account.kt +++ b/backend/src/main/kotlin/com/infendro/account/routing/Account.kt @@ -1,6 +1,6 @@ package com.infendro.account.routing -import com.infendro.account.config.AuthenticationPrincipal +import com.infendro.account.config.Principal import com.infendro.account.dto.request.* import com.infendro.account.service.AccountService import io.ktor.http.HttpStatusCode.Companion.OK @@ -79,28 +79,28 @@ fun Routing.account() { authenticate { get { - val principal = call.principal()!! + val principal = call.principal()!! AccountService.getAll(principal) .also { call.respond(it) } } get { - val principal = call.principal()!! + val principal = call.principal()!! AccountService.getCurrent(principal) .also { call.respond(it) } } delete { - val principal = call.principal()!! + val principal = call.principal()!! AccountService.deleteCurrent(principal) call.respond(OK) } put { - val principal = call.principal()!! + val principal = call.principal()!! val request = call.receive() AccountService.putCurrentUsername(principal, call.sessions, request) @@ -108,7 +108,7 @@ fun Routing.account() { } put { - val principal = call.principal()!! + val principal = call.principal()!! val request = call.receive() AccountService.putCurrentPassword(principal, call.sessions, request) @@ -116,7 +116,7 @@ fun Routing.account() { } post { - val principal = call.principal()!! + val principal = call.principal()!! val request = call.receive() AccountService.postCurrentSecret(principal, call.sessions, request) @@ -124,14 +124,14 @@ fun Routing.account() { } get { - val principal = call.principal()!! + val principal = call.principal()!! AccountService.getCurrentSessionAll(principal) .also { call.respond(it) } } delete { resource -> - val principal = call.principal()!! + val principal = call.principal()!! val id = resource.id AccountService.deleteCurrentSessionId(principal, call.sessions, id) @@ -139,7 +139,7 @@ fun Routing.account() { } delete { resource -> - val principal = call.principal()!! + val principal = call.principal()!! val id = resource.id AccountService.deleteId(principal, id) @@ -147,7 +147,7 @@ fun Routing.account() { } put { resource -> - val principal = call.principal()!! + val principal = call.principal()!! val id = resource.parent.id val request = call.receive() diff --git a/backend/src/main/kotlin/com/infendro/account/routing/Session.kt b/backend/src/main/kotlin/com/infendro/account/routing/Session.kt index fbdecaa..c6a29ef 100644 --- a/backend/src/main/kotlin/com/infendro/account/routing/Session.kt +++ b/backend/src/main/kotlin/com/infendro/account/routing/Session.kt @@ -1,6 +1,6 @@ package com.infendro.account.routing -import com.infendro.account.config.AuthenticationPrincipal +import com.infendro.account.config.Principal import com.infendro.account.dto.request.PostSessionRequest import com.infendro.account.service.SessionService import io.ktor.http.HttpStatusCode.Companion.OK @@ -42,14 +42,14 @@ fun Routing.session() { authenticate { get { - val principal = call.principal()!! + val principal = call.principal()!! SessionService.getAll(principal) .also { call.respond(it) } } delete { resource -> - val principal = call.principal()!! + val principal = call.principal()!! val id = resource.id SessionService.deleteId(principal, id) @@ -57,14 +57,14 @@ fun Routing.session() { } get { - val principal = call.principal()!! + val principal = call.principal()!! SessionService.getCurrent(principal) .also { call.respond(it) } } delete { - val principal = call.principal()!! + val principal = call.principal()!! SessionService.deleteCurrent(principal, call.sessions) call.respond(OK) 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 edc565e..e622816 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/AccessService.kt @@ -1,6 +1,6 @@ package com.infendro.account.service -import com.infendro.account.config.AuthenticationPrincipal +import com.infendro.account.config.Principal import com.infendro.account.dto.response.AccessResponse import com.infendro.account.dto.response.PostAccessResponse import com.infendro.account.dto.response.toResponse @@ -14,7 +14,7 @@ import com.infendro.account.util.TokenGenerator import org.jetbrains.exposed.v1.core.eq object AccessService { - fun post(principal: AuthenticationPrincipal): PostAccessResponse { + fun post(principal: Principal): PostAccessResponse { if (principal.role != OWNER) throw ForbiddenException() @@ -28,7 +28,7 @@ object AccessService { ) } - fun getAll(principal: AuthenticationPrincipal): List { + fun getAll(principal: Principal): List { if (principal.role != OWNER) throw ForbiddenException() @@ -37,7 +37,7 @@ object AccessService { .map { it.toResponse() } } - fun deleteId(principal: AuthenticationPrincipal, id: Long) { + fun deleteId(principal: Principal, id: Long) { if (principal.role != OWNER) throw ForbiddenException() 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 f57a4db..434bd15 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/AccountService.kt @@ -1,7 +1,7 @@ package com.infendro.account.service -import com.infendro.account.config.AuthenticationPrincipal -import com.infendro.account.config.AuthenticationSession +import com.infendro.account.config.Principal +import com.infendro.account.config.Session import com.infendro.account.dto.request.* import com.infendro.account.dto.response.* import com.infendro.account.exception.client.ConflictException @@ -48,7 +48,7 @@ object AccountService { ) } - fun getAll(principal: AuthenticationPrincipal): List { + fun getAll(principal: Principal): List { if (principal.role != OWNER) throw ForbiddenException() @@ -57,18 +57,18 @@ object AccountService { .map(AccountEntity::toResponse) } - fun getCurrent(principal: AuthenticationPrincipal): AccountResponse { + fun getCurrent(principal: Principal): AccountResponse { return principal.account.toResponse() } - fun deleteCurrent(principal: AuthenticationPrincipal) { + fun deleteCurrent(principal: Principal) { if (principal.role == OWNER) throw ForbiddenException() AccountRepository.delete(principal.account) } - fun putCurrentUsername(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PutAccountCurrentUsernameRequest) { + fun putCurrentUsername(principal: Principal, sessions: CurrentSession, request: PutAccountCurrentUsernameRequest) { if (!OTP.verify(principal.account.secret, request.otp)) throw UnauthorizedException() @@ -77,10 +77,10 @@ object AccountService { } SessionRepository.delete(principal.account.sessions()) - sessions.clear() + sessions.clear() } - fun putCurrentPassword(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PutAccountCurrentPasswordRequest) { + fun putCurrentPassword(principal: Principal, sessions: CurrentSession, request: PutAccountCurrentPasswordRequest) { if (!OTP.verify(principal.account.secret, request.otp)) throw UnauthorizedException() @@ -92,10 +92,14 @@ object AccountService { } SessionRepository.delete(principal.account.sessions()) - sessions.clear() + sessions.clear() } - fun postCurrentSecret(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PostAccountCurrentSecretRequest): PostAccountCurrentSecretResponse { + fun postCurrentSecret( + principal: Principal, + sessions: CurrentSession, + request: PostAccountCurrentSecretRequest + ): PostAccountCurrentSecretResponse { if (principal.account.passwordHash != PasswordHasher.hash(request.password, principal.account.passwordSalt)) throw UnauthorizedException() @@ -105,19 +109,19 @@ object AccountService { } SessionRepository.delete(principal.account.sessions()) - sessions.clear() + sessions.clear() return PostAccountCurrentSecretResponse( secret = secret, ) } - fun getCurrentSessionAll(principal: AuthenticationPrincipal): List { + fun getCurrentSessionAll(principal: Principal): List { return principal.account.sessions() .map(SessionEntity::toResponse) } - fun deleteCurrentSessionId(principal: AuthenticationPrincipal, sessions: CurrentSession, id: Long) { + fun deleteCurrentSessionId(principal: Principal, sessions: CurrentSession, id: Long) { val session = SessionRepository .singleOrNull { SessionTable.id eq id } ?: throw NotFoundException() @@ -128,10 +132,10 @@ object AccountService { SessionRepository.delete(session) if (session.id == principal.session.id) - sessions.clear() + sessions.clear() } - fun deleteId(principal: AuthenticationPrincipal, id: Long) { + fun deleteId(principal: Principal, id: Long) { val account = AccountRepository .singleOrNull { AccountTable.id eq id } ?: throw NotFoundException() @@ -142,7 +146,7 @@ object AccountService { AccountRepository.delete(account) } - fun putIdRole(principal: AuthenticationPrincipal, id: Long, request: PutAccountIdRoleRequest) { + fun putIdRole(principal: Principal, id: Long, request: PutAccountIdRoleRequest) { val account = AccountRepository .singleOrNull { AccountTable.id eq id } ?: throw NotFoundException() 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 4f1ce35..1fa5dbc 100644 --- a/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt +++ b/backend/src/main/kotlin/com/infendro/account/service/SessionService.kt @@ -1,7 +1,7 @@ package com.infendro.account.service -import com.infendro.account.config.AuthenticationPrincipal -import com.infendro.account.config.AuthenticationSession +import com.infendro.account.config.Principal +import com.infendro.account.config.Session import com.infendro.account.dto.request.PostSessionRequest import com.infendro.account.dto.response.SessionResponse import com.infendro.account.dto.response.toResponse @@ -40,10 +40,10 @@ object SessionService { this.account = account } - sessions.set(AuthenticationSession(token)) + sessions.set(Session(token)) } - fun getAll(principal: AuthenticationPrincipal): List { + fun getAll(principal: Principal): List { if (principal.role == USER) throw ForbiddenException() @@ -53,7 +53,7 @@ object SessionService { .map(SessionEntity::toResponse) } - fun deleteId(principal: AuthenticationPrincipal, id: Long) { + fun deleteId(principal: Principal, id: Long) { if (principal.role == USER) throw ForbiddenException() @@ -67,13 +67,13 @@ object SessionService { SessionRepository.delete(session) } - fun getCurrent(principal: AuthenticationPrincipal): SessionResponse { + fun getCurrent(principal: Principal): SessionResponse { return principal.session.toResponse() } - fun deleteCurrent(principal: AuthenticationPrincipal, sessions: CurrentSession) { + fun deleteCurrent(principal: Principal, sessions: CurrentSession) { SessionRepository.delete(principal.session) - sessions.clear() + sessions.clear() } }