update user session on event

- update handleRemoteAccountEvent to update user session on change event in AccountListener
- add RedisSessionService
- add fromAccountResponse to RemoteAuthenticationUtil
- extract make remote authentication from RemoteAuthenticationManager to RemoteAuthenticationUtil
This commit is contained in:
2025-08-06 15:08:50 +02:00
committed by swordsteel
parent b8519ab1c5
commit ff54e06204
4 changed files with 102 additions and 31 deletions

View File

@@ -2,6 +2,7 @@ package ltd.hlaeja.listener
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import ltd.hlaeja.library.accountRegistry.event.AccountMessage import ltd.hlaeja.library.accountRegistry.event.AccountMessage
import ltd.hlaeja.service.RedisSessionService
import org.apache.kafka.clients.consumer.ConsumerRecord import org.apache.kafka.clients.consumer.ConsumerRecord
import org.springframework.kafka.annotation.KafkaListener import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -9,10 +10,15 @@ import org.springframework.stereotype.Component
val log = KotlinLogging.logger {} val log = KotlinLogging.logger {}
@Component @Component
class AccountListener { class AccountListener(
private val sessionService: RedisSessionService,
) {
@KafkaListener(topics = ["account"]) @KafkaListener(topics = ["account"])
fun handleRemoteAccountEvent(record: ConsumerRecord<String, AccountMessage>) { fun handleRemoteAccountEvent(record: ConsumerRecord<String, AccountMessage>) {
log.trace { "Received event: ${record.key()} for user: ${record.value().userId}" } log.trace { "Received event: ${record.key()} for user: ${record.value().userId}" }
if (record.key() == "change" && record.value().change.any { it in setOf("enabled", "username", "roles") }) {
sessionService.updateUser(record.value().userId).subscribe()
}
} }
} }

View File

@@ -1,14 +1,10 @@
package ltd.hlaeja.security.manager package ltd.hlaeja.security.manager
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.JwtException import io.jsonwebtoken.JwtException
import java.util.UUID
import ltd.hlaeja.jwt.service.PublicJwtService import ltd.hlaeja.jwt.service.PublicJwtService
import ltd.hlaeja.security.user.RemoteAuthentication
import ltd.hlaeja.security.user.RemoteUserDetail
import ltd.hlaeja.service.AccountRegistryService import ltd.hlaeja.service.AccountRegistryService
import ltd.hlaeja.util.RemoteAuthenticationUtil
import ltd.hlaeja.util.toAuthenticationRequest import ltd.hlaeja.util.toAuthenticationRequest
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.authentication.AuthenticationServiceException import org.springframework.security.authentication.AuthenticationServiceException
@@ -17,7 +13,6 @@ import org.springframework.security.authentication.event.AuthenticationFailureBa
import org.springframework.security.authentication.event.AuthenticationSuccessEvent import org.springframework.security.authentication.event.AuthenticationSuccessEvent
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -44,7 +39,7 @@ class RemoteAuthenticationManager(
private fun processToken( private fun processToken(
response: ltd.hlaeja.library.accountRegistry.Authentication.Response, response: ltd.hlaeja.library.accountRegistry.Authentication.Response,
): Authentication = try { ): Authentication = try {
publicJwtService.verify(response.token) { claims -> makeRemoteAuthentication(claims) } publicJwtService.verify(response.token) { claims -> RemoteAuthenticationUtil.fromJwtClaims(claims) }
} catch (e: JwtException) { } catch (e: JwtException) {
throw "An error occurred while processing token: ${e.message}" throw "An error occurred while processing token: ${e.message}"
.let { .let {
@@ -52,26 +47,4 @@ class RemoteAuthenticationManager(
AuthenticationServiceException(it, e) AuthenticationServiceException(it, e)
} }
} }
private fun makeRemoteAuthentication(
claims: Jws<Claims>,
) = RemoteAuthentication(
makeRemoteUserDetail(claims),
makeSimpleGrantedAuthorities(claims),
true,
)
private fun makeSimpleGrantedAuthorities(
claims: Jws<Claims>,
): MutableList<SimpleGrantedAuthority> = (claims.payload["role"] as String)
.split(",")
.map { SimpleGrantedAuthority("ROLE_$it") }
.toMutableList()
private fun makeRemoteUserDetail(
claims: Jws<Claims>,
): RemoteUserDetail = RemoteUserDetail(
UUID.fromString(claims.payload["id"] as String),
claims.payload["username"] as String,
)
} }

View File

@@ -0,0 +1,48 @@
package ltd.hlaeja.service
import java.util.UUID
import ltd.hlaeja.security.user.RemoteAuthentication
import ltd.hlaeja.util.RemoteAuthenticationUtil
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.RedisSession
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class RedisSessionService(
private val redisSessionRepository: ReactiveRedisIndexedSessionRepository,
private val accountRegistryService: AccountRegistryService,
) {
fun updateUser(
user: UUID,
): Flux<RedisSession> = findByUser(user)
.flatMapMany { map ->
getUserAccount(user).flatMapMany { response ->
Flux.fromIterable(map.values).map { session -> session to response }
}
}
.flatMap { (session: RedisSession, response: RemoteAuthentication) ->
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextImpl(response))
save(session)
}
private fun getUserAccount(
user: UUID,
) = accountRegistryService.getAccount(user)
.map(RemoteAuthenticationUtil::fromAccountResponse)
private fun findByUser(
user: UUID,
): Mono<MutableMap<String, RedisSession>> = redisSessionRepository.findByPrincipalName(user.toString())
.doOnNext { ltd.hlaeja.listener.log.trace { "Found ${it.size} session(s) for user $user" } }
.filter { map -> map.isNotEmpty() }
private fun save(
session: RedisSession,
): Mono<RedisSession> = redisSessionRepository.save(session)
.thenReturn(session)
.doOnNext { ltd.hlaeja.listener.log.trace { "Save session: ${it.id}" } }
}

View File

@@ -0,0 +1,44 @@
package ltd.hlaeja.util
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import java.util.UUID
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.security.user.RemoteAuthentication
import ltd.hlaeja.security.user.RemoteUserDetail
import org.springframework.security.core.authority.SimpleGrantedAuthority
object RemoteAuthenticationUtil {
fun fromJwtClaims(
claims: Jws<Claims>,
): RemoteAuthentication = makeRemoteAuthentication(
id = UUID.fromString(claims.payload["id"] as String),
username = claims.payload["username"] as String,
roles = (claims.payload["role"] as String).split(","),
authenticated = true,
)
fun fromAccountResponse(
response: Account.Response,
): RemoteAuthentication = makeRemoteAuthentication(
id = response.id,
username = response.username,
roles = response.roles,
authenticated = response.enabled,
)
private fun makeRemoteAuthentication(
id: UUID,
username: String,
roles: List<String>,
authenticated: Boolean,
) = RemoteAuthentication(
remoteUserDetail = RemoteUserDetail(
id = id,
username = username,
),
authorities = roles.map { SimpleGrantedAuthority("ROLE_$it") }.toMutableList(),
authenticated = authenticated,
)
}