Skip to content

Commit

Permalink
#345 | Admin session handling improved (#355)
Browse files Browse the repository at this point in the history
* All POST and DELETE requests should now refresh session token
token expiration increased to 5 hours

* Small fix for version service

Co-authored-by: szymon.owczarzak <[email protected]>
  • Loading branch information
szymon-owczarzak and szymon.owczarzak authored Mar 26, 2021
1 parent 8e6a215 commit 78c4df6
Show file tree
Hide file tree
Showing 26 changed files with 304 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.cognifide.cogboard.CogboardConstants.Companion.PROP_USER
import com.cognifide.cogboard.config.EndpointsConfig.Companion.CREDENTIALS_PROP
import com.cognifide.cogboard.config.service.CredentialsService
import com.cognifide.cogboard.config.service.EndpointsService
import com.cognifide.cogboard.config.utils.JsonUtils.findById
import com.cognifide.cogboard.utils.ExtensionFunctions.findById
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.cognifide.cogboard.config.controller
import com.cognifide.cogboard.CogboardConstants
import com.cognifide.cogboard.config.EndpointsConfig.Companion.ENDPOINT_ID_PROP
import com.cognifide.cogboard.config.service.EndpointsService
import com.cognifide.cogboard.config.utils.JsonUtils.findAllByKeyValue
import com.cognifide.cogboard.utils.ExtensionFunctions.findAllByKeyValue
import io.vertx.core.AbstractVerticle
import io.vertx.core.eventbus.Message
import io.vertx.core.json.JsonArray
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.cognifide.cogboard.config.handler

import io.vertx.core.http.HttpMethod
import io.vertx.reactivex.ext.web.RoutingContext

class HandlerUtil {
companion object {
fun endResponse(body: String, routingContext: RoutingContext) {
routingContext.response()
.putHeader("Content-Type", "application/json")
.end(body)
if (SESSION_REFRESHERS.contains(routingContext.request().method())) {
routingContext.request().headers().add("body", body)
routingContext.reroute(HttpMethod.POST, "/api/session/refresh")
} else routingContext.response()
.putHeader("Content-Type", "application/json")
.end(body)
}

val SESSION_REFRESHERS = setOf(HttpMethod.POST, HttpMethod.DELETE)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cognifide.cogboard.config.handler

import com.cognifide.cogboard.CogboardConstants
import com.cognifide.cogboard.utils.ExtensionFunctions.endEmptyJson
import io.knotx.server.api.handler.RoutingHandlerFactory
import io.vertx.core.Handler
import io.vertx.core.json.JsonObject
Expand All @@ -17,6 +18,6 @@ class VersionHandler : RoutingHandlerFactory {
?.publish(CogboardConstants.EVENT_VERSION_CONFIG, JsonObject())
event
.response()
.end(JsonObject().toString())
.endEmptyJson()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import com.cognifide.cogboard.config.CredentialsConfig.Companion.CREDENTIALS_ARR
import com.cognifide.cogboard.config.CredentialsConfig.Companion.CREDENTIAL_ID_PREFIX
import com.cognifide.cogboard.config.CredentialsConfig.Companion.CREDENTIAL_ID_PROP
import com.cognifide.cogboard.config.CredentialsConfig.Companion.CREDENTIAL_LABEL_PROP
import com.cognifide.cogboard.config.utils.JsonUtils.findById
import com.cognifide.cogboard.config.utils.JsonUtils.getObjectPositionById
import com.cognifide.cogboard.config.utils.JsonUtils.putIfNotExist
import com.cognifide.cogboard.utils.ExtensionFunctions.findById
import com.cognifide.cogboard.utils.ExtensionFunctions.getObjectPositionById
import com.cognifide.cogboard.utils.ExtensionFunctions.putIfNotExist
import com.cognifide.cogboard.config.validation.credentials.CredentialsValidator
import com.cognifide.cogboard.storage.Storage
import com.cognifide.cogboard.storage.VolumeStorageFactory.credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import com.cognifide.cogboard.config.EndpointsConfig.Companion.ENDPOINTS_ARRAY
import com.cognifide.cogboard.config.EndpointsConfig.Companion.ENDPOINT_ID_PREFIX
import com.cognifide.cogboard.config.EndpointsConfig.Companion.ENDPOINT_ID_PROP
import com.cognifide.cogboard.config.EndpointsConfig.Companion.ENDPOINT_LABEL_PROP
import com.cognifide.cogboard.config.utils.JsonUtils.findById
import com.cognifide.cogboard.config.utils.JsonUtils.getObjectPositionById
import com.cognifide.cogboard.config.utils.JsonUtils.putIfNotExist
import com.cognifide.cogboard.utils.ExtensionFunctions.findById
import com.cognifide.cogboard.utils.ExtensionFunctions.getObjectPositionById
import com.cognifide.cogboard.utils.ExtensionFunctions.putIfNotExist
import com.cognifide.cogboard.config.validation.endpoints.EndpointsValidator
import com.cognifide.cogboard.storage.Storage
import com.cognifide.cogboard.storage.VolumeStorageFactory.endpoints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,26 @@ class VersionService {
isNewer(latestVersion, runningVersion)

fun checkVersion(body: JsonObject): Boolean {
val latestVersion = body.getString("tag_name")?.substring(1) ?: ""
val repoLatestVersion = body.getString("tag_name")?.substring(1) ?: ""
if (repoLatestVersion.isValid()) {
this.latestVersion = repoLatestVersion
}

this.lastCheck = LocalDateTime.now()
return if (isNewer(latestVersion, runningVersion)) {
this.latestVersion = latestVersion
this.latestResponse = body
true
} else false
}

private fun String.isValid() = this.matches(VALID_PATTERN)

companion object {
const val YEAR_INIT = 2000
const val DAY_INIT = 1
const val HOUR_INIT = 0
const val MINUTE_INIT = 0
val VALID_PATTERN = Regex("\\d+\\.\\d+\\.\\d+")

fun isNewer(newValue: String, oldValue: String = "0.0.0"): Boolean {
val v1parts = newValue.split('.').map { it.toInt() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.cognifide.cogboard.security

import com.cognifide.cogboard.utils.ExtensionFunctions.toJWT
import io.knotx.server.api.security.AuthHandlerFactory
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.KeyStoreOptions
import io.vertx.ext.auth.jwt.JWTAuthOptions
import io.vertx.reactivex.core.Vertx
import io.vertx.reactivex.ext.auth.jwt.JWTAuth
import io.vertx.reactivex.ext.web.handler.AuthHandler
Expand All @@ -15,7 +15,6 @@ class JwtAuthHandlerFactory : AuthHandlerFactory {

override fun create(vertx: Vertx?, config: JsonObject?): AuthHandler {
val keyStoreOptions = KeyStoreOptions(config)
val jwtAuthOptions = JWTAuthOptions().setKeyStore(keyStoreOptions)
return JWTAuthHandler.create(JWTAuth.create(vertx, jwtAuthOptions))
return JWTAuthHandler.create(JWTAuth.create(vertx, keyStoreOptions.toJWT()))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.cognifide.cogboard.security

import com.cognifide.cogboard.utils.ExtensionFunctions.asJsonObject
import com.cognifide.cogboard.utils.ExtensionFunctions.endEmptyJson
import com.cognifide.cogboard.utils.ExtensionFunctions.toJWT
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.KeyStoreOptions
import io.vertx.ext.jwt.JWTOptions
import io.vertx.reactivex.core.Vertx
import io.vertx.reactivex.ext.auth.jwt.JWTAuth
import io.vertx.reactivex.ext.web.RoutingContext

open class JwtCommon {

protected lateinit var jwtAuth: JWTAuth

protected fun sendJWT(ctx: RoutingContext, user: String) {
val body = ctx.request().getHeader("body") ?: ""
ctx.response().putHeader("token", generateJWT(user))
if (body.isNotEmpty()) ctx.response().end(body)
else ctx.response().endEmptyJson()
}

protected open fun init(vertx: Vertx, config: JsonObject) {
jwtAuth = initJWT(vertx, config)
}

private fun generateJWT(username: String): String {
val token = jwtAuth.generateToken(
username.asJsonObject("name"),
JWTOptions().setExpiresInSeconds(SESSION_DURATION_IN_SECONDS)
) ?: "no data"
return "Bearer $token"
}

private fun initJWT(vertx: Vertx, config: JsonObject): JWTAuth {
val options = KeyStoreOptions()
.setType(config.getString("type", "jceks"))
.setPath(config.getString("path", "keystore.jceks"))
.setPassword(config.getString("password", "secret"))

return JWTAuth.create(vertx, options.toJWT())
}

companion object {
const val SESSION_DURATION_IN_SECONDS = 5 * 60 * 60 // hours * min * sec
const val DEFAULT_ERROR = "Unable to authenticate"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,23 @@ import com.cognifide.cogboard.storage.Storage
import com.cognifide.cogboard.storage.VolumeStorageFactory
import io.knotx.server.api.handler.RoutingHandlerFactory
import io.vertx.core.Handler
import io.vertx.core.VertxException
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.KeyStoreOptions
import io.vertx.ext.auth.jwt.JWTAuthOptions
import io.vertx.ext.jwt.JWTOptions
import io.vertx.reactivex.core.Vertx
import io.vertx.reactivex.ext.auth.jwt.JWTAuth
import io.vertx.reactivex.ext.web.RoutingContext

class LoginHandler(val storage: Storage = VolumeStorageFactory.admin()) : RoutingHandlerFactory {
class LoginHandler(val storage: Storage = VolumeStorageFactory.admin()) : RoutingHandlerFactory, JwtCommon() {

private var vertx: Vertx? = null
private lateinit var config: JsonObject
private lateinit var wrongUserMsg: String
private lateinit var wrongPassMsg: String

override fun getName(): String = "login-handler"

override fun create(vertx: Vertx?, config: JsonObject?): Handler<RoutingContext> {
this.vertx = vertx
this.config = config ?: JsonObject()
val wrongUserMsg = config?.getString("wrongUserMsg") ?: "Please, enter correct Username"
val wrongPassMsg = config?.getString("wrongPassMsg") ?: "Please, enter correct Password"
if (vertx == null || config == null) {
throw VertxException("Unable to create LoginHandler vertex=$vertx, config=$config")
}
init(vertx, config)

return Handler { ctx ->
ctx.bodyAsJson?.let {
Expand All @@ -44,6 +41,12 @@ class LoginHandler(val storage: Storage = VolumeStorageFactory.admin()) : Routin
}
}

override fun init(vertx: Vertx, config: JsonObject) {
super.init(vertx, config)
wrongUserMsg = config.getString("wrongUserMsg", DEFAULT_ERROR)
wrongPassMsg = config.getString("wrongPassMsg", DEFAULT_ERROR)
}

private fun getAdmin(name: String): Admin? {
val admin = storage.loadConfig()
val username = admin.getString(PROP_USER)
Expand All @@ -54,33 +57,9 @@ class LoginHandler(val storage: Storage = VolumeStorageFactory.admin()) : Routin
}

private fun isAuthorized(admin: Admin, password: String?) =
admin.password.isNotBlank() && admin.password == password
admin.password.isNotBlank() && admin.password == password

private fun sendUnauthorized(ctx: RoutingContext, message: String) {
ctx.response().setStatusMessage(message).setStatusCode(STATUS_CODE_401).end()
}

private fun sendJWT(ctx: RoutingContext, user: String) {
ctx.response().end(generateJWT(user))
}

private fun generateJWT(username: String): String {
val keyStore = KeyStoreOptions()
.setType(config.getString("type", "jceks"))
.setPath(config.getString("path", "keystore.jceks"))
.setPassword(config.getString("password", "secret"))

val config = JWTAuthOptions().setKeyStore(keyStore)
val jwtAuth = JWTAuth.create(vertx, config)

val token = jwtAuth?.generateToken(
JsonObject().put("name", username),
JWTOptions().setExpiresInSeconds(SESSION_DURATION_IN_SECONDS)
) ?: "no data"
return "{\"token\":\"Bearer $token\"}"
}

companion object {
private const val SESSION_DURATION_IN_SECONDS = 2 * 60 * 60 // hours * min * sec
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.cognifide.cogboard.security

import com.cognifide.cogboard.CogboardConstants.Companion.STATUS_CODE_401
import com.cognifide.cogboard.utils.ExtensionFunctions.asJsonObject
import com.cognifide.cogboard.storage.Storage
import com.cognifide.cogboard.storage.VolumeStorageFactory
import io.knotx.server.api.handler.RoutingHandlerFactory
import io.vertx.core.Handler
import io.vertx.core.VertxException
import io.vertx.core.json.JsonObject
import io.vertx.reactivex.core.Vertx
import io.vertx.reactivex.ext.web.RoutingContext

class SessionHandler(val storage: Storage = VolumeStorageFactory.admin()) : RoutingHandlerFactory, JwtCommon() {

private lateinit var sessionRefreshError: String

override fun getName(): String = "session-handler"

override fun create(vertx: Vertx?, config: JsonObject?): Handler<RoutingContext> {
if (vertx == null || config == null) {
throw VertxException("Unable to create SessionHandler vertex=$vertx, config=$config")
}
init(vertx, config)

return Handler { ctx ->
val token = ctx
.request()
.getHeader("Authorization")
?.substringAfter("Bearer ")
?.asJsonObject("jwt")

jwtAuth.authenticate(token) {
val username = it.result().delegate.principal().getString("name") ?: ""
if (it.succeeded() && username.isNotBlank()) {
sendJWT(ctx, username)
} else sendUnauthorized(ctx, sessionRefreshError)
}
}
}

override fun init(vertx: Vertx, config: JsonObject) {
super.init(vertx, config)
sessionRefreshError = config.getString("sessionRefreshError", DEFAULT_ERROR)
}

private fun sendUnauthorized(ctx: RoutingContext, message: String) {
ctx.response().setStatusMessage(message).setStatusCode(STATUS_CODE_401).end()
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package com.cognifide.cogboard.config.utils
package com.cognifide.cogboard.utils

import com.cognifide.cogboard.CogboardConstants
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.KeyStoreOptions
import io.vertx.ext.auth.jwt.JWTAuthOptions
import io.vertx.reactivex.core.http.HttpServerResponse
import java.util.stream.Collectors

object JsonUtils {
object ExtensionFunctions {

fun JsonArray.findById(idValue: String, idKey: String = CogboardConstants.PROP_ID): JsonObject {
return this.findByKeyValue(idValue, idKey)
}

fun String.asJsonObject(propName: String): JsonObject = JsonObject().put(propName, this)

fun HttpServerResponse.endEmptyJson() {
this.end(JsonObject().toString())
}

fun KeyStoreOptions.toJWT(): JWTAuthOptions = JWTAuthOptions().setKeyStore(this)

private fun JsonArray.findByKeyValue(value: String, key: String): JsonObject {
return this.stream()
.map { it as JsonObject }
Expand Down Expand Up @@ -44,4 +55,12 @@ object JsonUtils {
}
return this
}

fun String.makeUrlPublic(publicDomain: String): String {
if (this == "") return ""
if (publicDomain == "") return this

val rest = this.substringAfter("//").substringAfter("/")
return "${publicDomain.removeSuffix("/")}/$rest"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ class DeleteWidget : RoutingHandlerFactory {
vertx
?.eventBus()
?.publish(CogboardConstants.EVENT_DELETE_WIDGET_CONFIG, event.body.toJsonObject())
event
.response()
.end(config?.getJsonObject("body", DEFAULT_NO_BODY)?.encode())
}

companion object {
val DEFAULT_NO_BODY: JsonObject = JsonObject().put("status", "failed")
event.reroute("/api/session/refresh")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ class UpdateWidget : RoutingHandlerFactory {
vertx
?.eventBus()
?.publish(CogboardConstants.EVENT_UPDATE_WIDGET_CONFIG, event.body.toJsonObject())
event
.response()
.end(config?.getJsonObject("body", DEFAULT_NO_BODY)?.encode())
}

companion object {
val DEFAULT_NO_BODY: JsonObject = JsonObject().put("status", "failed")
event.reroute("/api/session/refresh")
}
}
Loading

0 comments on commit 78c4df6

Please sign in to comment.