Skip to content

Commit

Permalink
Remove permissions from cookie (#214)
Browse files Browse the repository at this point in the history
* remove permissions from cookie
  • Loading branch information
makselivanov authored Nov 27, 2023
1 parent 08a0a8b commit d6d43f7
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 129 deletions.
1 change: 1 addition & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
implementation("io.ktor:ktor-server-auth:$ktorVersion")
implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion")
implementation("io.ktor:ktor-server-sessions:$ktorVersion")
implementation("io.ktor:ktor-server-call-logging:$ktorVersion")

implementation("ch.qos.logback:logback-classic:$logbackVersion")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.util.logging.KtorSimpleLogger
import org.tod87et.roomkn.server.plugins.configureAuthentication
import org.tod87et.roomkn.server.plugins.configureCORS
import org.tod87et.roomkn.server.plugins.configureCleanup
import org.tod87et.roomkn.server.plugins.configureCallLogging
import org.tod87et.roomkn.server.plugins.configureRouting
import org.tod87et.roomkn.server.plugins.configureSerialization

Expand All @@ -18,5 +19,6 @@ fun Application.module() {
configureRouting()
configureSerialization()
configureCleanup()
configureCallLogging()
logger.info("RooMKN main module has been initialized")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.util.logging.Logger
import io.ktor.utils.io.CancellationException
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Date
import kotlinx.coroutines.delay
import kotlinx.datetime.toKotlinInstant
import org.tod87et.roomkn.server.database.ConstraintViolationException
Expand All @@ -13,9 +16,6 @@ import org.tod87et.roomkn.server.models.permissions.UserPermission
import org.tod87et.roomkn.server.models.users.LoginUserInfo
import org.tod87et.roomkn.server.models.users.RegistrationUserInfo
import org.tod87et.roomkn.server.models.users.UnregisteredUserInfo
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Date

class AccountControllerImpl(
private val log: Logger,
Expand All @@ -33,27 +33,12 @@ class AccountControllerImpl(

private val signAlgorithm = Algorithm.HMAC256(config.secret)

override val jwtVerifier: JWTVerifier = JWT
.require(Algorithm.HMAC256(config.secret))
.withAudience(config.audience)
.withIssuer(config.issuer)
.build()
override val jwtVerifier: JWTVerifier =
JWT.require(Algorithm.HMAC256(config.secret)).withAudience(config.audience).withIssuer(config.issuer).build()

override fun authenticateUser(loginUserInfo: LoginUserInfo): Result<AuthSession> {
val credentials = config.credentialsDatabase.getCredentialsInfoByUsername(loginUserInfo.username)
.getOrElse { ex ->
return when (ex) {
is MissingElementException -> {
Result.failure(NoSuchUserException(loginUserInfo.username, ex))
}

else -> {
Result.failure(ex)
}
}
}
val permissions = config.database.getUserPermissions(credentials.id)
.getOrElse { ex ->
val credentials =
config.credentialsDatabase.getCredentialsInfoByUsername(loginUserInfo.username).getOrElse { ex ->
return when (ex) {
is MissingElementException -> {
Result.failure(NoSuchUserException(loginUserInfo.username, ex))
Expand All @@ -72,7 +57,7 @@ class AccountControllerImpl(
val passwordHash = digest.digest()
return if (passwordHash.contentEquals(credentials.passwordHash)) {
log.debug("Authentication successful for `${loginUserInfo.username}`")
Result.success(AuthSession(createToken(credentials.id, permissions)))
Result.success(AuthSession(createToken(credentials.id)))
} else {
log.debug("Authentication failed for `${loginUserInfo.username}`")
Result.failure(AuthFailedException("Wrong username or password"))
Expand All @@ -83,20 +68,18 @@ class AccountControllerImpl(
registerUser(userInfo, defaultPermissions)

override fun validateSession(session: AuthSession): Result<Boolean> {
runCatching { jwtVerifier.verify(session.token) }
.getOrElse {
log.debug("Token verification failed", it)
return Result.success(false)
}
runCatching { jwtVerifier.verify(session.token) }.getOrElse {
log.debug("Token verification failed", it)
return Result.success(false)
}
digest.update(session.token.encodeToByteArray())
return config.credentialsDatabase.checkTokenWasInvalidated(digest.digest()).map { !it }
}

override fun invalidateSession(session: AuthSession): Result<Unit> {
digest.update(session.token.encodeToByteArray())
return config.credentialsDatabase.invalidateToken(
digest.digest(),
JWT.decode(session.token).expiresAtAsInstant.toKotlinInstant()
digest.digest(), JWT.decode(session.token).expiresAtAsInstant.toKotlinInstant()
)
}

Expand All @@ -113,8 +96,8 @@ class AccountControllerImpl(
return
}

val email = System.getenv(ENV_ROOMKN_SUPERUSER_EMAIL)?.takeUnless(String::isBlank)
?: System.getenv(ENV_HOST)?.takeUnless(String::isEmpty)?.let { "admin@$it" }
val email = System.getenv(ENV_ROOMKN_SUPERUSER_EMAIL)?.takeUnless(String::isBlank) ?: System.getenv(ENV_HOST)
?.takeUnless(String::isEmpty)?.let { "admin@$it" }

if (email == null || config.credentialsDatabase.getCredentialsInfoByEmail(email).isSuccess) {
log.warn(
Expand All @@ -124,8 +107,7 @@ class AccountControllerImpl(
}

val res = registerUser(
UnregisteredUserInfo(username, email, password),
defaultAdminPermissions
UnregisteredUserInfo(username, email, password), defaultAdminPermissions
)

if (res.isFailure) {
Expand Down Expand Up @@ -156,10 +138,7 @@ class AccountControllerImpl(

return when (ex) {
is ConstraintViolationException -> {
if (
ex.constraint == ConstraintViolationException.Constraint.USERNAME ||
ex.constraint == ConstraintViolationException.Constraint.EMAIL
) {
if (ex.constraint == ConstraintViolationException.Constraint.USERNAME || ex.constraint == ConstraintViolationException.Constraint.EMAIL) {
Result.failure(RegistrationFailedException("User with such username already exists"))
} else {
Result.failure(ex)
Expand All @@ -173,7 +152,10 @@ class AccountControllerImpl(
}

log.debug("User `${info.username}` has been registered")
return Result.success(AuthSession(createToken(info.id, defaultPermissions)))

config.database.updateUserPermissions(info.id, permissions)
log.debug("Set user `{}` permissions to {}", info.username, permissions)
return Result.success(AuthSession(createToken(info.id)))
}

override suspend fun cleanerLoop() {
Expand All @@ -190,12 +172,9 @@ class AccountControllerImpl(
}
}

private fun createToken(userId: Int, permissions: List<UserPermission>): String {
return JWT.create()
.withAudience(config.audience)
.withIssuer(config.issuer)
private fun createToken(userId: Int): String {
return JWT.create().withAudience(config.audience).withIssuer(config.issuer)
.withClaim(AuthSession.USER_ID_CLAIM_NAME, userId)
.withClaim(AuthSession.USER_PERMISSIONS_CLAIM_NAME, permissions.map { it.toString() })
.withExpiresAt(Date(System.currentTimeMillis() + config.tokenValidityPeriod.inWholeMilliseconds))
.sign(signAlgorithm)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.tod87et.roomkn.server.auth

import io.ktor.server.config.ApplicationConfig
import org.tod87et.roomkn.server.database.CredentialsDatabase
import org.tod87et.roomkn.server.database.Database
import org.tod87et.roomkn.server.util.checkField
import java.util.Base64
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import org.tod87et.roomkn.server.database.CredentialsDatabase
import org.tod87et.roomkn.server.database.Database
import org.tod87et.roomkn.server.util.checkField

class AuthConfig(
val issuer: String,
val issuer: String,
val audience: String,
val secret: ByteArray,
val pepper: ByteArray,
Expand All @@ -33,7 +33,7 @@ class AuthConfig(
var saltSize: Int = DEFAULT_SALT_SIZE,
var hashingAlgorithmId: String = DEFAULT_HASHING_ALGORITHM_ID,
val cleanupInterval: Duration = DEFAULT_CLEANUP_INTERVAL,
) {
) {
companion object {
private val DEFAULT_TOKEN_VALIDITY_PERIOD: Duration = 30.days
private const val DEFAULT_SALT_SIZE: Int = 32
Expand All @@ -56,7 +56,8 @@ class AuthConfig(

fun secret(secret: ByteArray) = apply { this.secret = secret }

fun secret(base64EncodedSecret: String) = apply { this.secret = Base64.getDecoder().decode(base64EncodedSecret) }
fun secret(base64EncodedSecret: String) =
apply { this.secret = Base64.getDecoder().decode(base64EncodedSecret) }

fun database(database: Database) = apply { this.database = database }

Expand All @@ -65,7 +66,8 @@ class AuthConfig(

fun pepper(pepper: ByteArray) = apply { this.pepper = pepper }

fun pepper(base64EncodedPepper: String) = apply { this.pepper = Base64.getDecoder().decode(base64EncodedPepper) }
fun pepper(base64EncodedPepper: String) =
apply { this.pepper = Base64.getDecoder().decode(base64EncodedPepper) }

fun tokenValidityPeriod(tokenValidityPeriod: Duration) =
apply { this.tokenValidityPeriod = tokenValidityPeriod }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@ import org.tod87et.roomkn.server.models.permissions.UserPermission
data class AuthSession(val token: String) : Principal {
companion object {
const val USER_ID_CLAIM_NAME: String = "userId"
const val USER_PERMISSIONS_CLAIM_NAME: String = "permissions"
}
}

val AuthSession.userId: Int get() = JWT.decode(token).getClaim(AuthSession.USER_ID_CLAIM_NAME).asInt()
?: throw SecurityException("Cannot decode user id from token")

val AuthSession.permissions: List<UserPermission>
get() = JWT.decode(token)
.getClaim(AuthSession.USER_PERMISSIONS_CLAIM_NAME)
.asList(String::class.java)
?.mapNotNull {
runCatching { UserPermission.valueOf(it) }.getOrNull()
} ?: emptyList()

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.tod87et.roomkn.server.plugins

import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.request.httpMethod
import io.ktor.server.request.uri
import org.slf4j.event.Level

fun Application.configureCallLogging() {
install(CallLogging) {
level = Level.INFO
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val userAgent = call.request.headers["User-Agent"]
val route = call.request.uri
"Status: $status, HTTP method: $httpMethod, User agent: $userAgent, Route: $route"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import io.ktor.server.routing.route
import io.ktor.util.pipeline.PipelineContext
import org.tod87et.roomkn.server.auth.AuthSession
import org.tod87et.roomkn.server.auth.AuthenticationProvider
import org.tod87et.roomkn.server.auth.permissions
import org.tod87et.roomkn.server.auth.userId
import org.tod87et.roomkn.server.database.ConstraintViolationException
import org.tod87et.roomkn.server.database.Database
Expand Down Expand Up @@ -91,7 +90,7 @@ private fun Route.reservationDeleteRouting(database: Database) {
.getOrElse {
return@delete call.handleReservationException(it)
}
call.requirePermissionOrSelf(reservation.userId) { return@delete call.onMissingPermission() }
call.requirePermissionOrSelf(reservation.userId, database) { return@delete call.onMissingPermission() }

val result = database.deleteReservation(id)
result
Expand Down Expand Up @@ -121,12 +120,10 @@ private fun Route.reserveRouting(database: Database) {

private inline fun ApplicationCall.requirePermissionOrSelf(
self: Int,
database: Database,
onPermissionMissing: () -> Nothing
) {
val session = principal<AuthSession>()
if (session == null || session.userId != self && !session.permissions.contains(UserPermission.ReservationsAdmin)) {
onPermissionMissing()
}
requirePermissionOrSelfImpl(self, database, UserPermission.ReservationsAdmin, onPermissionMissing)
}

private suspend fun ApplicationCall.handleReservationException(ex: Throwable) {
Expand Down
Loading

0 comments on commit d6d43f7

Please sign in to comment.