From: Lanius Trolling Date: Sun, 5 May 2024 14:30:22 +0000 (-0400) Subject: Rework NationStates login token storage, as well as make sessions perishable X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=ac30c031b52300df2bf562157dd7418e2aacf066;p=factbooks Rework NationStates login token storage, as well as make sessions perishable --- diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt index c0ab266..0fb6763 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/SessionStorage.kt @@ -5,10 +5,12 @@ import info.mechyrdia.data.* import io.ktor.server.sessions.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.time.Instant object SessionStorageMongoDB : SessionStorage { override suspend fun invalidate(id: String) { - SessionStorageDoc.Table.del(Id(id)) + if (!SessionStorageDoc.Table.del(Id(id))) + throw NoSuchElementException("Session $id not found") } override suspend fun read(id: String): String { @@ -27,10 +29,14 @@ data class SessionStorageDoc( @SerialName(MONGODB_ID_KEY) override val id: Id, val session: UserSession, + + val expiresAt: @Serializable(with = InstantSerializer::class) Instant = Instant.now().plusSeconds(1_814_400), ) : DataDocument { companion object : TableHolder { override val Table = DocumentTable() - override suspend fun initialize() = Unit + override suspend fun initialize() { + Table.expire(SessionStorageDoc::expiresAt) + } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt b/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt index 03ce61f..e81e3cf 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/auth/ViewsLogin.kt @@ -2,8 +2,7 @@ package info.mechyrdia.auth import com.github.agadar.nationstates.shard.NationShard import info.mechyrdia.Configuration -import info.mechyrdia.data.Id -import info.mechyrdia.data.NationData +import info.mechyrdia.data.* import info.mechyrdia.lore.page import info.mechyrdia.lore.redirectHref import info.mechyrdia.lore.standardNavBar @@ -15,30 +14,58 @@ import io.ktor.server.plugins.* import io.ktor.server.sessions.* import io.ktor.util.* import kotlinx.html.* -import java.util.concurrent.ConcurrentHashMap +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.Instant import kotlin.collections.set val PageDoNotCacheAttributeKey = AttributeKey("Mechyrdia.PageDoNotCache") -private val nsTokenMap = ConcurrentHashMap() +@Serializable +data class NsStoredToken( + @SerialName(MONGODB_ID_KEY) + override val id: Id = Id(), + val verifyToken: String = token(), + + val expiresAt: @Serializable(with = InstantSerializer::class) Instant = Instant.now().plusSeconds(1800L), +) : DataDocument { + companion object : TableHolder { + override val Table = DocumentTable() + + override suspend fun initialize() { + Table.expire(NsStoredToken::expiresAt) + } + + suspend fun createToken(): NsLoginToken { + return NsStoredToken().also { Table.put(it) }.let { (id, token) -> + NsLoginToken(id.id, token) + } + } + + suspend fun verifyToken(id: String): String? { + return Table.get(Id(id))?.verifyToken?.also { + Table.del(Id(id)) + } + } + } +} + +data class NsLoginToken(val id: String, val token: String) suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { attributes.put(PageDoNotCacheAttributeKey, true) + val (tokenId, nsToken) = NsStoredToken.createToken() + return page("Log In With NationStates", standardNavBar()) { - val tokenKey = token() - val nsToken = token() - - nsTokenMap[tokenKey] = nsToken - section { h1 { +"Log In With NationStates" } form(method = FormMethod.post, action = href(Root.Auth.LoginPost())) { installCsrfToken() hiddenInput { - name = "token" - value = tokenKey + name = "tokenId" + value = tokenId } label { @@ -70,10 +97,10 @@ suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { } } -suspend fun ApplicationCall.loginRoute(nation: String, checksum: String, token: String): Nothing { +suspend fun ApplicationCall.loginRoute(nation: String, checksum: String, tokenId: String): Nothing { val nationId = nation.toNationId() - val nsToken = nsTokenMap.remove(token) - ?: throw MissingRequestParameterException("token") + val nsToken = NsStoredToken.verifyToken(tokenId) + ?: throw MissingRequestParameterException("tokenId") val nationData = if (nationId == Configuration.Current.ownerNation && checksum == Configuration.Current.emergencyPassword) NationData.get(Id(nationId)) diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt index f2d4320..0829658 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Comments.kt @@ -10,7 +10,7 @@ import java.time.Instant @Serializable data class Comment( @SerialName(MONGODB_ID_KEY) - override val id: Id, + override val id: Id = Id(), val submittedBy: Id, val submittedIn: String, @@ -42,7 +42,7 @@ data class Comment( @Serializable data class CommentReplyLink( @SerialName(MONGODB_ID_KEY) - override val id: Id = Id(), + override val id: Id, val originalPost: Id, val replyingPost: Id, diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt index bb6b356..bfc810e 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Data.kt @@ -10,6 +10,7 @@ import com.mongodb.kotlin.client.coroutine.expireAfter import com.mongodb.reactivestreams.client.MongoClients import com.mongodb.reactivestreams.client.gridfs.GridFSBucket import com.mongodb.reactivestreams.client.gridfs.GridFSBuckets +import info.mechyrdia.auth.NsStoredToken import info.mechyrdia.auth.SessionStorageDoc import info.mechyrdia.auth.WebDavToken import info.mechyrdia.robot.RobotGlobals @@ -186,8 +187,8 @@ class DocumentTable>(private val kClass: KClass) { return collection().find(Filters.eq(MONGODB_ID_KEY, id)).singleOrNull() } - suspend fun del(id: Id) { - collection().deleteOne(Filters.eq(MONGODB_ID_KEY, id)) + suspend fun del(id: Id): Boolean { + return collection().deleteOne(Filters.eq(MONGODB_ID_KEY, id)).deletedCount > 0L } suspend fun all(): Flow { @@ -272,6 +273,7 @@ interface TableHolder> { val entries = listOf( SessionStorageDoc, NationData, + NsStoredToken, WebDavToken, Comment, CommentReplyLink, diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt index e19bb69..788075c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsComment.kt @@ -71,7 +71,6 @@ suspend fun ApplicationCall.newCommentRoute(pagePathParts: List, content val now = Instant.now() val comment = Comment( - id = Id(), submittedBy = loggedInAs.id, submittedIn = pagePathParts.joinToString("/"), submittedAt = now, diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt b/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt index 614cd2e..e678ed8 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/Visits.kt @@ -28,7 +28,7 @@ data class PageVisitTotals( @Serializable data class PageVisitData( @SerialName(MONGODB_ID_KEY) - override val id: Id = Id(), + override val id: Id, val path: String, val visitor: String, diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt index 3bdd9d9..dea60c4 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceBodies.kt @@ -7,7 +7,7 @@ import kotlinx.html.textArea import kotlinx.serialization.Serializable @Serializable -class LoginPayload(override val csrfToken: String? = null, val nation: String, val checksum: String, val token: String) : CsrfProtectedResourcePayload +class LoginPayload(override val csrfToken: String? = null, val nation: String, val checksum: String, val tokenId: String) : CsrfProtectedResourcePayload @Serializable class LogoutPayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt index 1b30ea2..b1e33b5 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceTypes.kt @@ -135,7 +135,7 @@ class Root(val error: String? = null) : ResourceHandler, ResourceFilter { with(auth) { filterCall() } with(payload) { call.verifyCsrfToken() } - call.loginRoute(payload.nation, payload.checksum, payload.token) + call.loginRoute(payload.nation, payload.checksum, payload.tokenId) } }