diff --git a/src/main/kotlin/com/restapi/Main.kt b/src/main/kotlin/com/restapi/Main.kt index a4d76a9..98b385d 100644 --- a/src/main/kotlin/com/restapi/Main.kt +++ b/src/main/kotlin/com/restapi/Main.kt @@ -13,6 +13,7 @@ import com.restapi.controllers.Entities import com.restapi.domain.DataNotFoundException import com.restapi.domain.Session import com.restapi.domain.Session.currentTenant +import com.restapi.domain.Session.currentToken import com.restapi.domain.Session.currentUser import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.redis @@ -93,7 +94,7 @@ fun main(args: Array) { val iamClient = it.queryParam("client") ?: appConfig.iamClient() val ep = getAuthEndpoint().tokenEndpoint - val client = HttpClient.newHttpClient() + val httpClient = HttpClient.newHttpClient() val req = HttpRequest.newBuilder() .uri(URI.create(ep)) .POST( @@ -110,7 +111,7 @@ fun main(args: Array) { ) .header("Content-Type", "application/x-www-form-urlencoded") .build() - val message = client.send(req, BodyHandlers.ofString()).body() + val message = httpClient.send(req, BodyHandlers.ofString()).body() val atResponse = objectMapper.readValue(message) val parsed = validateAuthToken(atResponse.accessToken) @@ -125,6 +126,71 @@ fun main(args: Array) { Session.jwk() ) } + + post("/refresh") { ctx -> + //refresh authToken + val authToken = ctx.header("Authorization") + ?.replace("Bearer ", "") + ?.replace("Bearer: ", "") + ?.trim() ?: throw UnauthorizedResponse() + + val authUser = validateAuthToken(authToken, skipValidate = true) + val client = ctx.queryParam("client") ?: throw BadRequestResponse("client not sent") + val redirectUri = ctx.queryParam("redirectUri") ?: throw BadRequestResponse("redirectUri not sent") + + val key = "AUTH_TOKEN_${authUser.userName}" + val found = redis.llen(key) + val foundOldAt = (0..found) + .mapNotNull { redis.lindex(key, it) } + .map { objectMapper.readValue(it) } + .firstOrNull { it.accessToken == authToken } ?: throw BadRequestResponse("authToken not found in cache") + + val expiresAt = foundOldAt.createdAt.plusSeconds(foundOldAt.expiresIn + 0L) + val rtExpiresAt = foundOldAt.createdAt.plusSeconds(foundOldAt.refreshExpiresIn + 0L) + + val now = LocalDateTime.now() + + //we can refresh if at is expired, but we still have time for refresh + if (expiresAt.isBefore(now) && now.isBefore(rtExpiresAt)) { + logger.warn("We can refresh the token for ${authUser.userName}, expires = $expiresAt, refresh Till = $rtExpiresAt") + val ep = getAuthEndpoint().tokenEndpoint + val httpClient = HttpClient.newHttpClient() + val req = HttpRequest.newBuilder() + .uri(URI.create(ep)) + .POST( + BodyPublishers.ofString( + getFormDataAsString( + mapOf( + "refresh_token" to foundOldAt.refreshToken, + "redirect_uri" to redirectUri, + "client_id" to client, + "grant_type" to "refresh_token", + ) + ) + ) + ) + .header("Content-Type", "application/x-www-form-urlencoded") + .build() + val message = httpClient.send(req, BodyHandlers.ofString()).body() + val atResponse = objectMapper.readValue(message) + val parsed = validateAuthToken(atResponse.accessToken) + + //keep track of this + redis.rpush("AUTH_TOKEN_${parsed.userName}", message) + + ctx.json(atResponse) + } else { + //at is still valid + if (expiresAt.isAfter(now)) { + logger.warn("Still valid, the token for ${authUser.userName}") + ctx.json(foundOldAt) + } else { + //we have exceeded the refresh time, so we shall ask the user to login again + logger.warn("We can't refresh the token for ${authUser.userName}, as refresh-time is expired") + throw UnauthorizedResponse() + } + } + } } before("/api/*") { ctx -> @@ -166,7 +232,9 @@ fun main(args: Array) { path("/api") { post("/audit/{action}") { logger.warn("User ${currentUser()} of tenant ${currentTenant()} has performed ${it.pathParam("action")} @ ${LocalDateTime.now()}") + it.json(mapOf("status" to true)) } + post("/script/database/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps)) post("/script/{file}/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps)) diff --git a/src/main/kotlin/com/restapi/config/Auth.kt b/src/main/kotlin/com/restapi/config/Auth.kt index 3cf58c7..66db38b 100644 --- a/src/main/kotlin/com/restapi/config/Auth.kt +++ b/src/main/kotlin/com/restapi/config/Auth.kt @@ -44,11 +44,15 @@ object Auth { .setVerificationKeyResolver(HttpsJwksVerificationKeyResolver(HttpsJwks(getAuthEndpoint().jwksUri))) .build() + private val jwtConsumerSkipValidate = JwtConsumerBuilder() + .setSkipAllValidators() + .setVerificationKeyResolver(HttpsJwksVerificationKeyResolver(HttpsJwks(getAuthEndpoint().jwksUri))) + .build() - fun validateAuthToken(authToken: String): AuthUser { + fun validateAuthToken(authToken: String, skipValidate: Boolean = false): AuthUser { // Validate the JWT and process it to the Claims - val jwtClaims = jwtConsumer.process(authToken) + val jwtClaims = if (skipValidate) jwtConsumerSkipValidate.process(authToken) else jwtConsumer.process(authToken) val userId = jwtClaims.jwtClaims.claimsMap["preferred_username"] as String val tenant = jwtClaims.jwtClaims.claimsMap["tenant"] as String val roles = ((jwtClaims.jwtClaims.claimsMap["realm_access"] as Map)["roles"]) as List @@ -56,13 +60,14 @@ object Auth { return AuthUser( userName = userId, tenant = tenant, - roles = roles + roles = roles, + token = authToken ) } } -data class AuthUser(val userName: String, val tenant: String, val roles: List) +data class AuthUser(val userName: String, val tenant: String, val roles: List, val token: String) enum class Action { CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN } diff --git a/src/main/kotlin/com/restapi/domain/db.kt b/src/main/kotlin/com/restapi/domain/db.kt index 4af4f5a..4e28306 100644 --- a/src/main/kotlin/com/restapi/domain/db.kt +++ b/src/main/kotlin/com/restapi/domain/db.kt @@ -37,7 +37,7 @@ object Session { private val logger = LoggerFactory.getLogger("session") private val currentUser = object : ThreadLocal() { override fun initialValue(): AuthUser { - return AuthUser("", "", emptyList()) + return AuthUser("", "", emptyList(), "") } } @@ -159,6 +159,7 @@ object Session { fun currentUser() = currentUser.get().userName fun currentTenant() = currentUser.get().tenant fun currentRoles() = currentUser.get().roles + fun currentToken() = currentUser.get().token fun jwk() = keypair.toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY) fun Database.findByEntityAndId(entity: String, id: String): DataModel {