keep track of auth token in db

This commit is contained in:
gowthaman.b 2024-05-08 13:27:02 +05:30
parent 182ad004fb
commit ef181bce1a
4 changed files with 117 additions and 32 deletions

View File

@ -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<String, AuthEndpoint>()
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<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()
)
)
)
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<AuthTokenResponse>(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")
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,16 +260,11 @@ 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()
}
}
}
}

View File

@ -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())
@ -258,7 +258,9 @@ enum class AddressType {
data class ContactPerson(
val id: String = UUID.randomUUID().toString(),
val name: String = "", val email: String = "", val mobile: String = "")
val name: String = "", val email: String = "", val mobile: String = ""
)
data class Address(
val id: String = UUID.randomUUID().toString(),
val type: AddressType = AddressType.BILLING,
@ -684,3 +686,18 @@ open class Plant : BaseModel() {
@DbJsonB
var prefixes: MutableMap<String, String>? = 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
}

View File

@ -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)
);

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<createTable name="auth_token_cache" pkName="pk_auth_token_cache">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" defaultValue="0" notnull="true"/>
<column name="required_approval_levels" type="integer" defaultValue="0" notnull="true"/>
<column name="approval_status" type="varchar(8)" defaultValue="'APPROVED'" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_auth_token_cache_approval_status"/>
<column name="tags" type="varchar[]" defaultValue="'{}'" notnull="true"/>
<column name="comments" type="jsonb" defaultValue="'[]'" notnull="true"/>
<column name="auth_token" type="text" notnull="true"/>
<column name="issued_at" type="localdatetime" notnull="true"/>
<column name="expires_at" type="localdatetime" notnull="true"/>
<column name="refresh_expires_at" type="localdatetime" notnull="true"/>
<column name="refresh_token" type="text" notnull="true"/>
<column name="user_id" type="varchar" notnull="true"/>
<column name="expired" type="boolean" defaultValue="false" notnull="true"/>
<column name="logged_out" type="boolean" defaultValue="false" notnull="true"/>
<column name="deleted" type="boolean" defaultValue="false" notnull="true"/>
<column name="version" type="integer" defaultValue="1" notnull="true"/>
<column name="created_at" type="localdatetime" defaultValue="'now()'" notnull="true"/>
<column name="modified_at" type="localdatetime" defaultValue="'now()'" notnull="true"/>
<column name="created_by" type="varchar" notnull="true"/>
<column name="modified_by" type="varchar" notnull="true"/>
</createTable>
</changeSet>
</migration>