There are several scenarios, where I want to update the user/principal data such that the changes are reflected while the user stays logged in (I do not want to force re-authentication)
From "within" the session this is not a Problem:
@PostMapping("/updateInfo")
fun updateMyData(
@AuthenticationPrincipal user: AppUser,
@Valid @RequestBody newInfo: UpdateDataRequest
): ResponseEntity<TestUserInfo> {
val testInfo = TestUserInfo(user, newInfo)
user.info = testInfo
val updatedUser = users.save(user)
return ResponseEntity.ok(updatedUser.info!!)
}
When I allow the user to for example change the their own data, I can easily access and change the @AuthenticationPrincipal
- in successive requests i can observe that the data is updated.
This is different when I need to change the user data from 'outside' the session.
use cases
There are 2 use cases for this:
a). an administrator changes user-data
b). the user confirms his email address
Now a). clearly happens from within another http-session where the principal is a user with some admin privileges. For b). you might ask, why this doesn't happen within a session: I want a simple one-time confirmation link, i.e. a get request. I cannot assume, that the user is logged in via a session on the device the confirmation link is opened. It wouldn't feel right to me, to do a separate preauthentication provider or something to get the user authenticated - then there will an unnecessary session opened on a browser that is never used again.
So in both cases, when I fetch the user via a JPArepository, update data, and save it back, the change is up to date in the databse - but the logged-in users don't know of that change, because their user data is stored in the http session and doesn't know that it needs to be updated.
Note that I am not using redis/spring-session anything - this is just a plain http session, so from my understanding I can not use FindByIndexNameSessionRepository
.
What I have tried
In spring-security issue #3849 it was suggested by rwinch to override
SecurityContextRepository
- however, there is no further information on how to do that exactly - I tried to understand the interface but couldn't get much further there.I tried to get through the responses tothe followinf SO post: How to reload authorities on user update with Spring Security (ignoring answers using redis.)
- the most upvoted answer by leo doesn't help, as mentioned in the comments there
- Aure77 suggests using
SessionRegistry
, which I tried to use also following bealdung - but to no avail: I cannot the right session, getallprincipals() is always empty when there is an active session for a logged in user. In case I had the right session I'm still not even sure how to move on from there, as Aure just suggests usingexpireNow()
which forces reauthentication - I want to avoid that. - alexkasko suggests something similar - from his I am thinking that maybe spring boot uses a thread-local securityContextRepository by default, and thats why i have no principals. He the suggests something that i haven'T yet understood - also the answers are quite old (2012) and I don'T feel very secure about trying to understand and apply that
- TwiN suggests using a
HandlerInterceptor
. Hasler Choo suggests a modified version with a hashset that seems to be more close to what i need. As described below - it has its problems though.
HandlerInterceptor
based approach
This is the only solution so far that I could successfully implement - but it doesn't seem very flexible. My implementation so far will only cover user-role changes.
Configuration:
@Configuration
class WebMvcConfig : WebMvcConfigurer {
@Autowired
private lateinit var updateUserDataInterceptor : UpdateUserDataInterceptor
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(updateUserDataInterceptor)
}
}
The HandlerInterceptor:
@Component
class UpdateUserDataInterceptor(
@Autowired
private val users: AppUserRepository
) : HandlerInterceptor {
private val usersToUpdate = ConcurrentHashMap.newKeySet<Long>()
fun markUpdate(user: AppUser) = usersToUpdate.add(user.id)
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
val auth = SecurityContextHolder.getContext().authentication
(auth.principal as? AppUser)?.apply {
synchronized(usersToUpdate) {
if (id in usersToUpdate) {
role = users.findById(id).get().role
usersToUpdate.remove(id)
}
}
}
return true
}
}
Instead of just updating the role, what I would rather like, is just replace the entire principle - but the principal is final
in the Authentication object.
So whenever a would wnat something else than the role updated, this has to specifically be mentioned here.
Remaining questions:
- Are there other solutions than the
HandlerInterceptor
? - Is there a
HandlerInterceptor
based solution, that allows me to fully update the principal object