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.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.AuthTokenCache
import com.restapi.domain.Plant import com.restapi.domain.Plant
import com.restapi.domain.Session import com.restapi.domain.Session
import com.restapi.domain.Session.database
import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.objectMapper
import io.javalin.http.BadRequestResponse import io.javalin.http.BadRequestResponse
import io.javalin.http.ContentType import io.javalin.http.ContentType
@ -45,7 +47,7 @@ object Auth {
private val logger = LoggerFactory.getLogger("Auth") private val logger = LoggerFactory.getLogger("Auth")
private val authCache = ConcurrentHashMap<String, AuthEndpoint>() private val authCache = ConcurrentHashMap<String, AuthEndpoint>()
fun getAuthEndpoint(): AuthEndpoint { private fun getAuthEndpoint(): AuthEndpoint {
return authCache.computeIfAbsent("AUTH") { return authCache.computeIfAbsent("AUTH") {
val wellKnown = "${appConfig.iamUrl()}/realms/${appConfig.iamRealm()}/.well-known/openid-configuration" val wellKnown = "${appConfig.iamUrl()}/realms/${appConfig.iamRealm()}/.well-known/openid-configuration"
val client = HttpClient.newHttpClient() val client = HttpClient.newHttpClient()
@ -180,14 +182,14 @@ object Auth {
val atResponse = objectMapper.readValue<AuthTokenResponse>(message) val atResponse = objectMapper.readValue<AuthTokenResponse>(message)
val parsed = validateAuthToken(atResponse.accessToken) val parsed = validateAuthToken(atResponse.accessToken)
//keep track of this for renewal when asked by client
Session.redis.lpush( database.save(AuthTokenCache().apply {
"$AUTH_TOKEN${parsed.userName}", objectMapper.writeValueAsString( this.userId = parsed.userName
atResponse.copy( this.authToken = atResponse.accessToken
createdAt = LocalDateTime.now() 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) 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 client = ctx.queryParam("client") ?: throw BadRequestResponse("client not sent")
val redirectUri = ctx.queryParam("redirectUri") ?: throw BadRequestResponse("redirectUri not sent") val redirectUri = ctx.queryParam("redirectUri") ?: throw BadRequestResponse("redirectUri not sent")
val key = "$AUTH_TOKEN${authUser.userName}" val foundOldAt = database.find(AuthTokenCache::class.java)
val found = Session.redis.llen(key) .where()
logger.warn("for user ${authUser.userName}, found from redis, $key => $found entries") .eq("userId", authUser.userName)
val foundOldAt = (0..found).mapNotNull { Session.redis.lindex(key, it) } .eq("expired", false)
.map { objectMapper.readValue<AuthTokenResponse>(it) }.firstOrNull { it.accessToken == authToken } .eq("loggedOut", false)
?: throw BadRequestResponse("authToken not found in cache") .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 createdAt = foundOldAt.createdAt
val expiresAt = createdAt.plusSeconds(foundOldAt.expiresIn + 0L) val expiresAt = foundOldAt.expiresAt
val rtExpiresAt = createdAt.plusSeconds(foundOldAt.refreshExpiresIn + 0L) val rtExpiresAt = foundOldAt.refreshExpiresAt
val now = LocalDateTime.now() 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 //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") logger.warn("We can refresh the token for ${authUser.userName}, expires = $expiresAt, refresh Till = $rtExpiresAt")
val ep = getAuthEndpoint().tokenEndpoint val ep = getAuthEndpoint().tokenEndpoint
val httpClient = HttpClient.newHttpClient() val httpClient = HttpClient.newHttpClient()
@ -244,14 +260,9 @@ object Auth {
ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN) ctx.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
} else { } else {
//at is still valid //at is still valid
if (expiresAt.isAfter(now)) { //we have exceeded the refresh time, so we shall ask the user to login again
logger.warn("Still valid, the token for ${authUser.userName}, will expire at $expiresAt") logger.warn("We can't refresh the token for ${authUser.userName}, as refresh-time [$rtExpiresAt] is expired")
ctx.result(foundOldAt.accessToken).contentType(ContentType.TEXT_PLAIN) throw UnauthorizedResponse()
} 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 io.ebean.annotation.Index
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.*
import javax.persistence.* import javax.persistence.*
data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now()) data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now())
@ -257,10 +257,12 @@ enum class AddressType {
} }
data class ContactPerson( data class ContactPerson(
val id : String = UUID.randomUUID().toString(), 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( data class Address(
val id : String = UUID.randomUUID().toString(), val id: String = UUID.randomUUID().toString(),
val type: AddressType = AddressType.BILLING, val type: AddressType = AddressType.BILLING,
val address: String = "", val address: String = "",
val pincode: String = "" val pincode: String = ""
@ -683,4 +685,19 @@ open class Plant : BaseModel() {
@DbJsonB @DbJsonB
var prefixes: MutableMap<String, String>? = mutableMapOf() 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>