refractor things

This commit is contained in:
gowthaman.b 2023-11-13 21:11:53 +05:30
parent 25a5852e65
commit 43279e56bf
4 changed files with 237 additions and 211 deletions

View File

@ -1,22 +1,14 @@
package com.restapi
import AuthTokenResponse
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.Action
import com.restapi.config.*
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.Auth.validateAuthToken
import com.restapi.config.Role
import com.restapi.config.Roles
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
import com.restapi.domain.Session.setAuthorizedUser
import com.restapi.domain.Session.signPayload
import io.ebean.DataIntegrityException
@ -29,13 +21,6 @@ import io.javalin.http.util.RateLimitUtil
import io.javalin.json.JavalinJackson
import org.jose4j.jwt.consumer.InvalidJwtException
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse.BodyHandlers
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.LocalDateTime
import java.util.*
@ -50,7 +35,6 @@ fun main(args: Array<String>) {
val createRole = Role.Standard(Action.CREATE)
val updateRole = Role.Standard(Action.UPDATE)
val approveOrRejectRole = Role.Standard(Action.APPROVE)
val AUTH_TOKEN = "AUTH_TOKEN_V2"
//todo, create roles in keycloak based on entity and actions
@ -76,137 +60,11 @@ fun main(args: Array<String>) {
}
.routes {
path("/auth") {
//for testing, development only
get("/endpoint") {
it.json(getAuthEndpoint())
}
get("/init") {
val endpoint = getAuthEndpoint().authorizationEndpoint
val redirectUrl =
"$endpoint?response_type=code&client_id=${appConfig.iamClient()}&redirect_uri=${appConfig.iamClientRedirectUri()}&scope=profile&state=1234zyx"
it.redirect(redirectUrl)
}
get("/code") {
val code = it.queryParam("code") ?: throw BadRequestResponse("not proper")
val redirectUri = it.queryParam("redirectUrl") ?: appConfig.iamClientRedirectUri()
val iamClient = it.queryParam("client") ?: appConfig.iamClient()
val ep = getAuthEndpoint().tokenEndpoint
val httpClient = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder()
.uri(URI.create(ep))
.POST(
BodyPublishers.ofString(
getFormDataAsString(
mapOf(
"code" to code,
"redirect_uri" to redirectUri,
"client_id" to iamClient,
"grant_type" to "authorization_code",
)
)
)
)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val message = httpClient.send(req, BodyHandlers.ofString()).body()
val atResponse = objectMapper.readValue<AuthTokenResponse>(message)
val parsed = validateAuthToken(atResponse.accessToken)
//keep track of this for renewal when asked by client
redis.lpush(
"$AUTH_TOKEN${parsed.userName}",
objectMapper.writeValueAsString(
atResponse.copy(
createdAt = LocalDateTime.now()
)
)
)
it.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
}
get("/keys") {
//for the UI to validate signature and response
it.json(
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)
logger.warn("for user ${authUser.userName}, found from redis, $key => $found entries")
val foundOldAt = (0..found)
.mapNotNull { redis.lindex(key, it) }
.map { objectMapper.readValue<AuthTokenResponse>(it) }
.firstOrNull { it.accessToken == authToken }
?: throw BadRequestResponse("authToken not found in cache")
val createdAt = foundOldAt.createdAt ?: throw BadRequestResponse("created at is missing")
val expiresAt = createdAt.plusSeconds(foundOldAt.expiresIn + 0L)
val rtExpiresAt = createdAt.plusSeconds(foundOldAt.refreshExpiresIn + 0L)
val now = LocalDateTime.now()
logger.warn("can we refresh the token for ${authUser.userName}, created = $createdAt expires = $expiresAt, refresh Till = $rtExpiresAt")
//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<AuthTokenResponse>(message)
val parsed = validateAuthToken(atResponse.accessToken)
redis.lpush(
"AUTH_TOKEN_${parsed.userName}",
objectMapper.writeValueAsString(
atResponse.copy(createdAt = LocalDateTime.now())
)
)
ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
} else {
//at is still valid
if (expiresAt.isAfter(now)) {
logger.warn("Still valid, the token for ${authUser.userName}, will expire at $expiresAt")
ctx.result(foundOldAt.accessToken).contentType(ContentType.TEXT_PLAIN)
} 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 [$rtExpiresAt] is expired")
throw UnauthorizedResponse()
}
}
}
get("/endpoint", Auth::endPoint)
get("/init", Auth::init)
get("/code", Auth::code)
get("/keys", Auth::keys)
post("/refresh", Auth::refreshToken)
}
before("/api/*") { ctx ->
@ -264,67 +122,13 @@ fun main(args: Array<String>) {
patch("/{entity}/{id}", Entities::patch, Roles(adminRole, updateRole))
delete("/{entity}/{id}", Entities::delete, Roles(adminRole, Role.Standard(Action.DELETE)))
}
}
.exception(DuplicateKeyException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Duplicate Data"
)
).status(HttpStatus.CONFLICT)
}
.exception(DataIntegrityException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "References Missing"
)
).status(HttpStatus.EXPECTATION_FAILED)
}
.exception(DataNotFoundException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Data Not Found"
)
).status(HttpStatus.NOT_FOUND)
}
.exception(IllegalArgumentException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Incorrect Data"
)
).status(HttpStatus.BAD_REQUEST)
}
.exception(JsonMappingException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Incorrect Data"
)
).status(HttpStatus.BAD_REQUEST)
}
.exception(InvalidJwtException::class.java) { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Login required"
)
).status(HttpStatus.UNAUTHORIZED)
}
.exception(DuplicateKeyException::class.java, Exceptions.dupKeyExceptionHandler)
.exception(DataIntegrityException::class.java, Exceptions.dataIntegrityException)
.exception(DataNotFoundException::class.java, Exceptions.dataNotFoundException)
.exception(IllegalArgumentException::class.java,Exceptions.illegalArgumentException)
.exception(JsonMappingException::class.java, Exceptions.jsonMappingException)
.exception(InvalidJwtException::class.java, Exceptions.invalidJwtException)
.start(appConfig.portNumber())
}
private fun getFormDataAsString(formData: Map<String, String>): String {
return formData.entries.joinToString("&") {
val key = URLEncoder.encode(it.key, StandardCharsets.UTF_8)
val value = URLEncoder.encode(it.value, StandardCharsets.UTF_8)
"$key=$value"
}
}

View File

@ -2,23 +2,43 @@ package com.restapi.config
import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.Session
import com.restapi.domain.Session.objectMapper
import io.javalin.http.BadRequestResponse
import io.javalin.http.ContentType
import io.javalin.http.Context
import io.javalin.http.UnauthorizedResponse
import io.javalin.security.RouteRole
import org.jose4j.jwk.HttpsJwks
import org.jose4j.jwt.consumer.JwtConsumerBuilder
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.concurrent.ConcurrentHashMap
const val AUTH_TOKEN = "AUTH_TOKEN_V2"
private fun getFormDataAsString(formData: Map<String, String>): String {
return formData.entries.joinToString("&") {
val key = URLEncoder.encode(it.key, StandardCharsets.UTF_8)
val value = URLEncoder.encode(it.value, StandardCharsets.UTF_8)
"$key=$value"
}
}
object Auth {
private val logger = LoggerFactory.getLogger("Auth")
private val authCache = ConcurrentHashMap<String, AuthEndpoint>()
fun getAuthEndpoint(): AuthEndpoint {
@ -69,9 +89,145 @@ object Auth {
)
}
fun keys(ctx: Context) {
ctx.json(Session.jwk())
}
fun endPoint(ctx: Context) {
ctx.json(getAuthEndpoint())
}
fun init(ctx: Context) {
val endpoint = getAuthEndpoint().authorizationEndpoint
val redirectUrl =
"$endpoint?response_type=code&client_id=${appConfig.iamClient()}&redirect_uri=${appConfig.iamClientRedirectUri()}&scope=profile&state=1234zyx"
ctx.redirect(redirectUrl)
}
fun code(ctx: Context) {
val code = ctx.queryParam("code") ?: throw BadRequestResponse("not proper")
val redirectUri = ctx.queryParam("redirectUrl") ?: appConfig.iamClientRedirectUri()
val iamClient = ctx.queryParam("client") ?: appConfig.iamClient()
val ep = getAuthEndpoint().tokenEndpoint
val httpClient = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder()
.uri(URI.create(ep))
.POST(
HttpRequest.BodyPublishers.ofString(
getFormDataAsString(
mapOf(
"code" to code,
"redirect_uri" to redirectUri,
"client_id" to iamClient,
"grant_type" to "authorization_code",
)
)
)
)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val message = httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body()
val atResponse = objectMapper.readValue<AuthTokenResponse>(message)
val parsed = validateAuthToken(atResponse.accessToken)
//keep track of this for renewal when asked by client
Session.redis.lpush(
"$AUTH_TOKEN${parsed.userName}",
objectMapper.writeValueAsString(
atResponse.copy(
createdAt = LocalDateTime.now()
)
)
)
ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
}
fun refreshToken(ctx: Context) {
//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 = Session.redis.llen(key)
logger.warn("for user ${authUser.userName}, found from redis, $key => $found entries")
val foundOldAt = (0..found)
.mapNotNull { Session.redis.lindex(key, it) }
.map { objectMapper.readValue<AuthTokenResponse>(it) }
.firstOrNull { it.accessToken == authToken }
?: throw BadRequestResponse("authToken not found in cache")
val createdAt = foundOldAt.createdAt ?: throw BadRequestResponse("created at is missing")
val expiresAt = createdAt.plusSeconds(foundOldAt.expiresIn + 0L)
val rtExpiresAt = createdAt.plusSeconds(foundOldAt.refreshExpiresIn + 0L)
val now = LocalDateTime.now()
logger.warn("can we refresh the token for ${authUser.userName}, created = $createdAt expires = $expiresAt, refresh Till = $rtExpiresAt")
//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(
HttpRequest.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, HttpResponse.BodyHandlers.ofString()).body()
val atResponse = objectMapper.readValue<AuthTokenResponse>(message)
val parsed = validateAuthToken(atResponse.accessToken)
Session.redis.lpush(
"AUTH_TOKEN_${parsed.userName}",
objectMapper.writeValueAsString(
atResponse.copy(createdAt = LocalDateTime.now())
)
)
ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
} else {
//at is still valid
if (expiresAt.isAfter(now)) {
logger.warn("Still valid, the token for ${authUser.userName}, will expire at $expiresAt")
ctx.result(foundOldAt.accessToken).contentType(ContentType.TEXT_PLAIN)
} 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 [$rtExpiresAt] is expired")
throw UnauthorizedResponse()
}
}
}
}
data class AuthUser(val userName: String, val tenant: String, val roles: List<String>, val token: String, val expiry: LocalDateTime)
data class AuthUser(
val userName: String,
val tenant: String,
val roles: List<String>,
val token: String,
val expiry: LocalDateTime
)
enum class Action {
CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN
}

View File

@ -1,4 +1,4 @@
package com.restapi.config
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime

View File

@ -0,0 +1,66 @@
package com.restapi.config
import com.fasterxml.jackson.databind.JsonMappingException
import com.restapi.domain.DataNotFoundException
import io.ebean.DataIntegrityException
import io.ebean.DuplicateKeyException
import io.javalin.http.ExceptionHandler
import io.javalin.http.HttpStatus
import org.jose4j.jwt.consumer.InvalidJwtException
import org.slf4j.LoggerFactory
object Exceptions {
private val logger = LoggerFactory.getLogger("Exception")
val dupKeyExceptionHandler = ExceptionHandler<DuplicateKeyException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Duplicate Data"
)
).status(HttpStatus.CONFLICT)
}
val dataIntegrityException = ExceptionHandler<DataIntegrityException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "References Missing"
)
).status(HttpStatus.EXPECTATION_FAILED)
}
val dataNotFoundException = ExceptionHandler<DataNotFoundException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Data Not Found"
)
).status(HttpStatus.NOT_FOUND)
}
val illegalArgumentException = ExceptionHandler<IllegalArgumentException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Incorrect Data"
)
).status(HttpStatus.BAD_REQUEST)
}
val jsonMappingException = ExceptionHandler<JsonMappingException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Incorrect Json"
)
).status(HttpStatus.BAD_REQUEST)
}
val invalidJwtException = ExceptionHandler<InvalidJwtException> { e, ctx ->
logger.warn("while processing ${ctx.path()}, exception ${e.message}", e)
ctx.json(
mapOf(
"error" to "Login required"
)
).status(HttpStatus.UNAUTHORIZED)
}
}