full reborn

This commit is contained in:
2021-11-24 13:52:27 +06:00
parent 0ac6b0a4df
commit 6a6a197041
246 changed files with 4327 additions and 6952 deletions

View File

@@ -0,0 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppJavaProjectPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":postssystem.features.auth.common")
api project(":postssystem.features.common.server")
}
}
}
}

View File

@@ -0,0 +1,121 @@
package dev.inmo.postssystem.features.auth.server
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService
import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator
import dev.inmo.postssystem.features.users.common.User
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.server.configurators.*
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.ktor.server.uniload
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.*
import io.ktor.sessions.*
import kotlinx.serialization.builtins.nullable
data class AuthUserPrincipal(
val user: User
) : Principal
fun User.principal() = AuthUserPrincipal(this)
class AuthenticationRoutingConfigurator(
private val authFeature: AuthFeature,
private val authTokensService: AuthTokensService
) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element {
override fun Route.invoke() {
route(authRootPathPart) {
post(authAuthPathPart) {
safely(
{
// TODO:: add error info
it.printStackTrace()
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
val creds = call.uniload(AuthCreds.serializer())
val tokenInfo = authFeature.auth(creds)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
}
}
}
post(authRefreshPathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
val refreshToken = call.uniload(RefreshToken.serializer())
val tokenInfo = authFeature.refresh(refreshToken)
if (tokenInfo == null) {
if (call.response.status() == null) {
call.respond(HttpStatusCode.Forbidden)
}
} else {
call.sessions.set(tokenSessionKey, tokenInfo.token)
call.unianswer(
AuthTokenInfo.serializer().nullable,
tokenInfo
)
}
}
}
post(authGetMePathPart) {
safely(
{
// TODO:: add error info
call.respond(
HttpStatusCode.InternalServerError,
"Something went wrong"
)
}
) {
call.unianswer(
User.serializer().nullable,
authFeature.getMe(
call.uniload(AuthToken.serializer())
)
)
}
}
}
}
override fun Authentication.Configuration.invoke() {
session<AuthToken> {
validate {
val result = authTokensService.getUserPrincipal(it)
if (result.isSuccess) {
result.getOrThrow().principal()
} else {
null
}
}
challenge { call.respond(HttpStatusCode.Unauthorized) }
}
}
}

View File

@@ -0,0 +1,23 @@
package dev.inmo.postssystem.features.auth.server
import dev.inmo.postssystem.features.auth.common.AuthToken
import dev.inmo.postssystem.features.common.common.Milliseconds
import dev.inmo.postssystem.features.auth.common.tokenSessionKey
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationSessionsConfigurator
import io.ktor.sessions.*
import java.util.concurrent.TimeUnit
class SessionAuthenticationConfigurator(
private val maxAge: Milliseconds
) : ApplicationSessionsConfigurator.Element {
private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge)
override fun Sessions.Configuration.invoke() {
cookie<AuthToken>(tokenSessionKey) {
cookie.maxAgeInSeconds = maxAgeInSeconds
serializer = object : SessionSerializer<AuthToken> {
override fun deserialize(text: String): AuthToken = AuthToken(text)
override fun serialize(session: AuthToken): String = session.string
}
}
}
}

View File

@@ -0,0 +1,22 @@
package dev.inmo.postssystem.features.auth.server.tokens
import com.soywiz.klock.DateTime
import dev.inmo.postssystem.features.auth.common.AuthToken
import dev.inmo.postssystem.features.auth.common.RefreshToken
import dev.inmo.postssystem.features.users.common.UserId
import dev.inmo.micro_utils.repos.CRUDRepo
data class AuthTokenModel(
val token: AuthToken,
val refreshToken: RefreshToken,
val userId: UserId,
val expiring: DateTime,
val die: DateTime
)
interface AuthTokensRepo : CRUDRepo<AuthTokenModel, AuthToken, AuthTokenModel> {
suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel?
suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean
suspend fun deleteDied(now: DateTime = DateTime.now())
}

View File

@@ -0,0 +1,18 @@
package dev.inmo.postssystem.features.auth.server.tokens
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.users.common.User
sealed class AuthTokenException : Exception()
object AuthTokenExpiredException : AuthTokenException()
object AuthTokenNotFoundException : AuthTokenException()
object UserNotFoundException : AuthTokenException()
interface AuthTokensService {
/**
* @return [User] or one of failure exceptions: [AuthTokenException]
*/
suspend fun getUserPrincipal(authToken: AuthToken): Result<User>
}

View File

@@ -0,0 +1,184 @@
package dev.inmo.postssystem.features.auth.server.tokens
import com.soywiz.klock.DateTime
import com.soywiz.klock.milliseconds
import dev.inmo.postssystem.features.auth.common.*
import dev.inmo.postssystem.features.common.common.Milliseconds
import dev.inmo.postssystem.features.users.common.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import kotlinx.coroutines.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction
private class ExposedAuthTokensRepo(
override val database: Database
) : AuthTokensRepo, AbstractExposedCRUDRepo<AuthTokenModel, AuthToken, AuthTokenModel>(
tableName = "ExposedAuthTokensRepo"
) {
private val tokenColumn = text("token")
private val refreshTokenColumn = text("refreshToken")
private val userIdColumn = long("userId")
private val expiringColumn = double("expiring")
private val dieColumn = double("die")
override val primaryKey: PrimaryKey = PrimaryKey(tokenColumn)
override val selectByIds: SqlExpressionBuilder.(List<AuthToken>) -> Op<Boolean> = {
tokenColumn.inList(it.map { it.string })
}
override val selectById: SqlExpressionBuilder.(AuthToken) -> Op<Boolean> = {
tokenColumn.eq(it.string)
}
override val ResultRow.asObject: AuthTokenModel
get() = AuthTokenModel(
AuthToken(get(tokenColumn)),
RefreshToken(get(refreshTokenColumn)),
UserId(get(userIdColumn)),
DateTime(get(expiringColumn)),
DateTime(get(dieColumn))
)
init {
initTable()
}
override fun insert(value: AuthTokenModel, it: InsertStatement<Number>) {
it[tokenColumn] = value.token.string
it[refreshTokenColumn] = value.refreshToken.string
it[userIdColumn] = value.userId.long
it[expiringColumn] = value.expiring.unixMillisDouble
it[dieColumn] = value.die.unixMillisDouble
}
override fun update(id: AuthToken, value: AuthTokenModel, it: UpdateStatement) {
it[tokenColumn] = value.token.string
it[refreshTokenColumn] = value.refreshToken.string
it[userIdColumn] = value.userId.long
it[expiringColumn] = value.expiring.unixMillisDouble
it[dieColumn] = value.die.unixMillisDouble
}
override fun InsertStatement<Number>.asObject(value: AuthTokenModel): AuthTokenModel = AuthTokenModel(
AuthToken(get(tokenColumn)),
RefreshToken(get(refreshTokenColumn)),
UserId(get(userIdColumn)),
DateTime(get(expiringColumn)),
DateTime(get(dieColumn))
)
override suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel? = transaction(database) {
select { refreshTokenColumn.eq(refreshToken.string) }.limit(1).firstOrNull() ?.asObject
}
override suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean = transaction {
deleteWhere { tokenColumn.eq(toRemove.string) } > 0 && insert {
insert(toInsert, it)
}.insertedCount > 0
}
override suspend fun deleteDied(
now: DateTime
) = transaction(database) {
val nowAsDouble = now.unixMillisDouble
val tokens = select { dieColumn.less(nowAsDouble) }.map { it[tokenColumn] }
deleteWhere { dieColumn.less(nowAsDouble) }
tokens
}.forEach {
deleteObjectsIdsChannel.emit(AuthToken(it))
}
}
class DefaultAuthTokensService(
private val authTokensRepo: AuthTokensRepo,
private val usersRepo: ReadUsersStorage,
private val userAuthenticator: UserAuthenticator,
private val tokenLifetime: Milliseconds,
private val cleaningScope: CoroutineScope
) : AuthTokensService, AuthFeature {
private val tokenDieLifetime = tokenLifetime * 2
constructor(
database: Database,
usersRepo: ReadUsersStorage,
userAuthenticator: UserAuthenticator,
tokenLifetime: Milliseconds,
cleaningScope: CoroutineScope
): this(
ExposedAuthTokensRepo(database),
usersRepo,
userAuthenticator,
tokenLifetime,
cleaningScope
)
init {
cleaningScope.launchSafelyWithoutExceptions {
while (isActive) {
authTokensRepo.deleteDied()
delay(tokenLifetime)
}
}
}
override suspend fun getUserPrincipal(authToken: AuthToken): Result<User> {
val authTokenModel = authTokensRepo.getById(authToken) ?: return Result.failure(AuthTokenNotFoundException)
return if (authTokenModel.expiring < DateTime.now()) {
Result.failure(AuthTokenExpiredException)
} else {
val user = usersRepo.getById(authTokenModel.userId) ?: return Result.failure(UserNotFoundException)
Result.success(user)
}
}
override suspend fun auth(creds: AuthCreds): AuthTokenInfo? {
val user = userAuthenticator(creds) ?: return null
val now = DateTime.now()
val preAuthTokenModel = AuthTokenModel(
AuthToken(),
RefreshToken(),
user.id,
now + tokenLifetime.milliseconds,
now + tokenDieLifetime.milliseconds
)
val tokenModel = authTokensRepo.create(preAuthTokenModel).firstOrNull() ?: return null
return AuthTokenInfo(
tokenModel.token,
tokenModel.refreshToken
)
}
override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? {
val previousAuthTokenModel = authTokensRepo.getByRefreshToken(refresh) ?: return null
val now = DateTime.now()
if (previousAuthTokenModel.die < now) {
authTokensRepo.deleteById(previousAuthTokenModel.token)
return null
}
val newAuthTokenModel = AuthTokenModel(
AuthToken(),
RefreshToken(),
previousAuthTokenModel.userId,
now + tokenLifetime.milliseconds,
now + tokenDieLifetime.milliseconds
)
return if (authTokensRepo.replaceToken(previousAuthTokenModel.token, newAuthTokenModel)) {
AuthTokenInfo(newAuthTokenModel.token, newAuthTokenModel.refreshToken)
} else {
null
}
}
override suspend fun getMe(authToken: AuthToken): User? {
return usersRepo.getById(
authTokensRepo.getById(authToken) ?.userId ?: return null
)
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.postssystem.features.auth.server.tokens
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.users.common.User
fun interface UserAuthenticator {
suspend operator fun invoke(authCreds: AuthCreds): User?
}

View File

@@ -0,0 +1,44 @@
package dev.inmo.postssystem.features.auth.server.tokens
import dev.inmo.postssystem.features.auth.common.AuthCreds
import dev.inmo.postssystem.features.users.common.*
import dev.inmo.micro_utils.repos.exposed.AbstractExposedReadCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import org.jetbrains.exposed.sql.*
private class ExposedUsersAuthenticationRepo(
override val database: Database,
private val usersRepo: ExposedUsersStorage
) : AbstractExposedReadCRUDRepo<UserId, AuthCreds>("UsersAuthentications") {
private val passwordColumn = text("password")
private val userIdColumn = long("userid").uniqueIndex() references usersRepo.userIdColumn
override val primaryKey: PrimaryKey = PrimaryKey(userIdColumn)
override val ResultRow.asObject: UserId
get() = UserId(get(userIdColumn))
override val selectById: SqlExpressionBuilder.(AuthCreds) -> Op<Boolean> = {
usersRepo.select {
usersRepo.usernameColumn.eq(it.username.string)
}.firstOrNull() ?.get(usersRepo.userIdColumn) ?.let { userId ->
userIdColumn.eq(userId).and(passwordColumn.eq(it.password))
} ?: Op.FALSE
}
init {
initTable()
uniqueIndex("${tableName}_user_password", userIdColumn, passwordColumn)
}
}
fun exposedUsersAuthenticator(
database: Database,
usersRepo: ExposedUsersStorage
): UserAuthenticator {
val usersAuthenticatorRepo = ExposedUsersAuthenticationRepo(database, usersRepo)
return UserAuthenticator {
val userId = usersAuthenticatorRepo.getById(it) ?: return@UserAuthenticator null
usersRepo.getById(userId)
}
}