From ff54e06204cc7de4d29bbbb2e6bfb6272b5c111a Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Wed, 6 Aug 2025 15:08:50 +0200 Subject: [PATCH] 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 --- .../ltd/hlaeja/listener/AccountListener.kt | 8 +++- .../manager/RemoteAuthenticationManager.kt | 33 ++----------- .../ltd/hlaeja/service/RedisSessionService.kt | 48 +++++++++++++++++++ .../hlaeja/util/RemoteAuthenticationUtil.kt | 44 +++++++++++++++++ 4 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt create mode 100644 src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt diff --git a/src/main/kotlin/ltd/hlaeja/listener/AccountListener.kt b/src/main/kotlin/ltd/hlaeja/listener/AccountListener.kt index 1530d1f..8b31a21 100644 --- a/src/main/kotlin/ltd/hlaeja/listener/AccountListener.kt +++ b/src/main/kotlin/ltd/hlaeja/listener/AccountListener.kt @@ -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) { 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() + } } } diff --git a/src/main/kotlin/ltd/hlaeja/security/manager/RemoteAuthenticationManager.kt b/src/main/kotlin/ltd/hlaeja/security/manager/RemoteAuthenticationManager.kt index 9d30c02..5539a31 100644 --- a/src/main/kotlin/ltd/hlaeja/security/manager/RemoteAuthenticationManager.kt +++ b/src/main/kotlin/ltd/hlaeja/security/manager/RemoteAuthenticationManager.kt @@ -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 = 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, - ) = RemoteAuthentication( - makeRemoteUserDetail(claims), - makeSimpleGrantedAuthorities(claims), - true, - ) - - private fun makeSimpleGrantedAuthorities( - claims: Jws, - ): MutableList = (claims.payload["role"] as String) - .split(",") - .map { SimpleGrantedAuthority("ROLE_$it") } - .toMutableList() - - private fun makeRemoteUserDetail( - claims: Jws, - ): RemoteUserDetail = RemoteUserDetail( - UUID.fromString(claims.payload["id"] as String), - claims.payload["username"] as String, - ) } diff --git a/src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt b/src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt new file mode 100644 index 0000000..1a5a68c --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/service/RedisSessionService.kt @@ -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 = 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> = 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 = redisSessionRepository.save(session) + .thenReturn(session) + .doOnNext { ltd.hlaeja.listener.log.trace { "Save session: ${it.id}" } } +} diff --git a/src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt b/src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt new file mode 100644 index 0000000..ebcabfd --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/util/RemoteAuthenticationUtil.kt @@ -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, + ): 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, + authenticated: Boolean, + ) = RemoteAuthentication( + remoteUserDetail = RemoteUserDetail( + id = id, + username = username, + ), + authorities = roles.map { SimpleGrantedAuthority("ROLE_$it") }.toMutableList(), + authenticated = authenticated, + ) +}