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 ltd.hlaeja.library.accountRegistry.event.AccountMessage
import ltd.hlaeja.service.RedisSessionService
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component
@@ -9,10 +10,15 @@ import org.springframework.stereotype.Component
val log = KotlinLogging.logger {}
@Component
class AccountListener {
class AccountListener(
private val sessionService: RedisSessionService,
) {
@KafkaListener(topics = ["account"])
fun handleRemoteAccountEvent(record: ConsumerRecord<String, AccountMessage>) {
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
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.JwtException
import java.util.UUID
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.util.RemoteAuthenticationUtil
import ltd.hlaeja.util.toAuthenticationRequest
import org.springframework.context.ApplicationEventPublisher
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.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@@ -34,7 +29,7 @@ class RemoteAuthenticationManager(
authentication: Authentication,
): Mono<Authentication> = accountRegistryService.authenticate(authentication.toAuthenticationRequest())
.map(::processToken)
.doOnNext { publisher.publishEvent(AuthenticationSuccessEvent(it)) }
.doOnNext { publisher.publishEvent(AuthenticationSuccessEvent(it)) }
.doOnError { ex ->
if (ex is AuthenticationException) {
publisher.publishEvent(AuthenticationFailureBadCredentialsEvent(authentication, ex))
@@ -44,7 +39,7 @@ class RemoteAuthenticationManager(
private fun processToken(
response: ltd.hlaeja.library.accountRegistry.Authentication.Response,
): Authentication = try {
publicJwtService.verify(response.token) { claims -> makeRemoteAuthentication(claims) }
publicJwtService.verify(response.token) { claims -> RemoteAuthenticationUtil.fromJwtClaims(claims) }
} catch (e: JwtException) {
throw "An error occurred while processing token: ${e.message}"
.let {
@@ -52,26 +47,4 @@ class RemoteAuthenticationManager(
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,
)
}