From ef181bce1a517f7eb01021130f2d61534b53cdf4 Mon Sep 17 00:00:00 2001 From: "gowthaman.b" Date: Wed, 8 May 2024 13:27:02 +0530 Subject: [PATCH] keep track of auth token in db --- src/main/kotlin/com/restapi/config/Auth.kt | 67 +++++++++++-------- src/main/kotlin/com/restapi/domain/models.kt | 25 +++++-- src/main/resources/dbmigration/1.30.sql | 28 ++++++++ .../dbmigration/model/1.30.model.xml | 29 ++++++++ 4 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 src/main/resources/dbmigration/1.30.sql create mode 100644 src/main/resources/dbmigration/model/1.30.model.xml diff --git a/src/main/kotlin/com/restapi/config/Auth.kt b/src/main/kotlin/com/restapi/config/Auth.kt index 9b00803..5979e2b 100644 --- a/src/main/kotlin/com/restapi/config/Auth.kt +++ b/src/main/kotlin/com/restapi/config/Auth.kt @@ -2,8 +2,10 @@ package com.restapi.config import com.fasterxml.jackson.module.kotlin.readValue import com.restapi.config.AppConfig.Companion.appConfig +import com.restapi.domain.AuthTokenCache import com.restapi.domain.Plant import com.restapi.domain.Session +import com.restapi.domain.Session.database import com.restapi.domain.Session.objectMapper import io.javalin.http.BadRequestResponse import io.javalin.http.ContentType @@ -45,7 +47,7 @@ object Auth { private val logger = LoggerFactory.getLogger("Auth") private val authCache = ConcurrentHashMap() - fun getAuthEndpoint(): AuthEndpoint { + private fun getAuthEndpoint(): AuthEndpoint { return authCache.computeIfAbsent("AUTH") { val wellKnown = "${appConfig.iamUrl()}/realms/${appConfig.iamRealm()}/.well-known/openid-configuration" val client = HttpClient.newHttpClient() @@ -180,14 +182,14 @@ object Auth { val atResponse = objectMapper.readValue(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() - ) - ) - ) + + database.save(AuthTokenCache().apply { + this.userId = parsed.userName + this.authToken = atResponse.accessToken + this.expiresAt = LocalDateTime.now().plusSeconds(atResponse.expiresIn.toLong()) + this.refreshToken = atResponse.refreshToken + this.refreshExpiresAt = LocalDateTime.now().plusSeconds(atResponse.refreshExpiresIn.toLong()) + }) ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN) } @@ -200,22 +202,36 @@ object Auth { 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(it) }.firstOrNull { it.accessToken == authToken } - ?: throw BadRequestResponse("authToken not found in cache") + val foundOldAt = database.find(AuthTokenCache::class.java) + .where() + .eq("userId", authUser.userName) + .eq("expired", false) + .eq("loggedOut", false) + .gt("refreshExpiresAt", LocalDateTime.now()) + .findList() + .onEach { + logger.warn("valid authToken for ${authUser.userName} is ${it.authToken}") + } + .firstOrNull { + it.authToken.equals(authToken, ignoreCase = true) + } ?: throw BadRequestResponse("we did not find an entry for this auth token $authToken") - 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 createdAt = foundOldAt.createdAt + val expiresAt = foundOldAt.expiresAt + val rtExpiresAt = foundOldAt.refreshExpiresAt val now = LocalDateTime.now() - logger.warn("can we refresh the token for ${authUser.userName}, created = $createdAt expires = $expiresAt, refresh Till = $rtExpiresAt") + logger.warn("can we refresh the token for ${authUser.userName}, created = $createdAt expires = $expiresAt, refresh Till = $rtExpiresAt") + + val authTokenValid = expiresAt.isAfter(now) + if (authTokenValid) { + ctx.result(authToken).contentType(ContentType.TEXT_PLAIN) + return + } //we can refresh if at is expired, but we still have time for refresh - if (expiresAt.isBefore(now) && now.isBefore(rtExpiresAt)) { + val refreshTokenValid = rtExpiresAt.isAfter(now) + if (refreshTokenValid) { logger.warn("We can refresh the token for ${authUser.userName}, expires = $expiresAt, refresh Till = $rtExpiresAt") val ep = getAuthEndpoint().tokenEndpoint val httpClient = HttpClient.newHttpClient() @@ -244,14 +260,9 @@ object Auth { 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() - } + //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() } } diff --git a/src/main/kotlin/com/restapi/domain/models.kt b/src/main/kotlin/com/restapi/domain/models.kt index 3e0a446..3a4aa87 100644 --- a/src/main/kotlin/com/restapi/domain/models.kt +++ b/src/main/kotlin/com/restapi/domain/models.kt @@ -9,7 +9,7 @@ import io.ebean.annotation.* import io.ebean.annotation.Index import java.time.LocalDate import java.time.LocalDateTime -import java.util.UUID +import java.util.* import javax.persistence.* data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now()) @@ -257,10 +257,12 @@ enum class AddressType { } data class ContactPerson( - val id : String = UUID.randomUUID().toString(), - val name: String = "", val email: String = "", val mobile: String = "") + val id: String = UUID.randomUUID().toString(), + val name: String = "", val email: String = "", val mobile: String = "" +) + data class Address( - val id : String = UUID.randomUUID().toString(), + val id: String = UUID.randomUUID().toString(), val type: AddressType = AddressType.BILLING, val address: String = "", val pincode: String = "" @@ -683,4 +685,19 @@ open class Plant : BaseModel() { @DbJsonB var prefixes: MutableMap? = mutableMapOf() +} + +@Entity +open class AuthTokenCache : BaseModel() { + @Column(columnDefinition = "text") + var authToken: String = "" + var issuedAt: LocalDateTime = LocalDateTime.now() + var expiresAt: LocalDateTime = LocalDateTime.now() + var refreshExpiresAt: LocalDateTime = LocalDateTime.now() + + @Column(columnDefinition = "text") + var refreshToken: String = "" + var userId: String = "" + var expired: Boolean = false + var loggedOut: Boolean = false } \ No newline at end of file diff --git a/src/main/resources/dbmigration/1.30.sql b/src/main/resources/dbmigration/1.30.sql new file mode 100644 index 0000000..6072314 --- /dev/null +++ b/src/main/resources/dbmigration/1.30.sql @@ -0,0 +1,28 @@ +-- apply changes +create table auth_token_cache ( + sys_pk bigint generated by default as identity not null, + deleted_on timestamp, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, + auth_token text not null, + issued_at timestamp not null, + expires_at timestamp not null, + refresh_expires_at timestamp not null, + refresh_token text not null, + expired boolean default false not null, + logged_out boolean default false not null, + deleted boolean default false not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, + deleted_by varchar(255), + approval_status varchar(8) default 'APPROVED' not null, + tags varchar[] default '{}' not null, + comments jsonb default '[]' not null, + user_id varchar(255) not null, + created_by varchar(255) not null, + modified_by varchar(255) not null, + constraint ck_auth_token_cache_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint pk_auth_token_cache primary key (sys_pk) +); + diff --git a/src/main/resources/dbmigration/model/1.30.model.xml b/src/main/resources/dbmigration/model/1.30.model.xml new file mode 100644 index 0000000..dbc53db --- /dev/null +++ b/src/main/resources/dbmigration/model/1.30.model.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file